From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- uriloader/base/moz.build | 43 + uriloader/base/nsCURILoader.idl | 18 + uriloader/base/nsDocLoader.cpp | 1612 +++++++++ uriloader/base/nsDocLoader.h | 390 ++ uriloader/base/nsIContentHandler.idl | 35 + uriloader/base/nsIDocumentLoader.idl | 35 + uriloader/base/nsITransfer.idl | 156 + uriloader/base/nsIURIContentListener.idl | 123 + uriloader/base/nsIURILoader.idl | 139 + uriloader/base/nsIWebProgress.idl | 185 + uriloader/base/nsIWebProgressListener.idl | 568 +++ uriloader/base/nsIWebProgressListener2.idl | 69 + uriloader/base/nsURILoader.cpp | 832 +++++ uriloader/base/nsURILoader.h | 219 ++ uriloader/docs/index.rst | 10 + uriloader/docs/uriloader.rst | 46 + uriloader/exthandler/ContentHandlerService.cpp | 263 ++ uriloader/exthandler/ContentHandlerService.h | 55 + uriloader/exthandler/DBusHelpers.h | 84 + uriloader/exthandler/ExtHandlerService.sys.mjs | 832 +++++ uriloader/exthandler/ExternalHelperAppChild.cpp | 93 + uriloader/exthandler/ExternalHelperAppChild.h | 44 + uriloader/exthandler/ExternalHelperAppParent.cpp | 477 +++ uriloader/exthandler/ExternalHelperAppParent.h | 118 + uriloader/exthandler/HandlerList.sys.mjs | 238 ++ uriloader/exthandler/HandlerServiceChild.h | 25 + uriloader/exthandler/HandlerServiceParent.cpp | 381 ++ uriloader/exthandler/HandlerServiceParent.h | 67 + uriloader/exthandler/PExternalHelperApp.ipdl | 29 + uriloader/exthandler/PHandlerService.ipdl | 62 + uriloader/exthandler/WebHandlerApp.sys.mjs | 185 + uriloader/exthandler/android/nsMIMEInfoAndroid.cpp | 340 ++ uriloader/exthandler/android/nsMIMEInfoAndroid.h | 62 + .../exthandler/android/nsOSHelperAppService.cpp | 61 + .../exthandler/android/nsOSHelperAppService.h | 38 + uriloader/exthandler/components.conf | 20 + uriloader/exthandler/docs/index.rst | 76 + uriloader/exthandler/mac/nsDecodeAppleFile.cpp | 361 ++ uriloader/exthandler/mac/nsDecodeAppleFile.h | 116 + uriloader/exthandler/mac/nsLocalHandlerAppMac.h | 26 + uriloader/exthandler/mac/nsLocalHandlerAppMac.mm | 78 + uriloader/exthandler/mac/nsMIMEInfoMac.h | 33 + uriloader/exthandler/mac/nsMIMEInfoMac.mm | 106 + uriloader/exthandler/mac/nsOSHelperAppService.h | 53 + uriloader/exthandler/mac/nsOSHelperAppService.mm | 589 +++ uriloader/exthandler/moz.build | 144 + uriloader/exthandler/nsCExternalHandlerService.idl | 32 + uriloader/exthandler/nsContentHandlerApp.h | 30 + uriloader/exthandler/nsDBusHandlerApp.cpp | 164 + uriloader/exthandler/nsDBusHandlerApp.h | 31 + .../exthandler/nsExternalHelperAppService.cpp | 3750 ++++++++++++++++++++ uriloader/exthandler/nsExternalHelperAppService.h | 571 +++ uriloader/exthandler/nsExternalProtocolHandler.cpp | 550 +++ uriloader/exthandler/nsExternalProtocolHandler.h | 37 + uriloader/exthandler/nsIContentDispatchChooser.idl | 40 + .../exthandler/nsIExternalHelperAppService.idl | 191 + .../exthandler/nsIExternalProtocolService.idl | 155 + uriloader/exthandler/nsIHandlerService.idl | 179 + .../exthandler/nsIHelperAppLauncherDialog.idl | 90 + uriloader/exthandler/nsISharingHandlerApp.idl | 11 + uriloader/exthandler/nsLocalHandlerApp.cpp | 158 + uriloader/exthandler/nsLocalHandlerApp.h | 59 + uriloader/exthandler/nsMIMEInfoChild.h | 54 + uriloader/exthandler/nsMIMEInfoImpl.cpp | 526 +++ uriloader/exthandler/nsMIMEInfoImpl.h | 224 ++ uriloader/exthandler/nsOSHelperAppServiceChild.cpp | 119 + uriloader/exthandler/nsOSHelperAppServiceChild.h | 48 + .../tests/HandlerServiceTestUtils.sys.mjs | 253 ++ uriloader/exthandler/tests/WriteArgument.cpp | 20 + .../tests/gtest/ExternalHelperAppServiceTest.cpp | 41 + uriloader/exthandler/tests/gtest/moz.build | 11 + .../tests/mochitest/FTPprotocolHandler.html | 16 + .../HelperAppLauncherDialog_chromeScript.js | 104 + uriloader/exthandler/tests/mochitest/blank.html | 11 + uriloader/exthandler/tests/mochitest/browser.ini | 130 + .../tests/mochitest/browser_auto_close_window.js | 342 ++ .../browser_auto_close_window_nodialog.js | 308 ++ .../tests/mochitest/browser_bad_download_dir.js | 90 + .../browser_download_always_ask_preferred_app.js | 25 + .../mochitest/browser_download_idn_blocklist.js | 47 + .../browser_download_open_with_internal_handler.js | 818 +++++ .../mochitest/browser_download_preferred_action.js | 296 ++ .../mochitest/browser_download_privatebrowsing.js | 78 + .../mochitest/browser_download_skips_dialog.js | 60 + .../mochitest/browser_download_spam_permissions.js | 158 + .../tests/mochitest/browser_download_urlescape.js | 75 + .../mochitest/browser_extension_correction.js | 222 ++ .../tests/mochitest/browser_filehandling_loop.js | 93 + ..._prompt_not_blocked_without_user_interaction.js | 70 + .../mochitest/browser_ftp_protocol_handlers.js | 105 + .../browser_launched_app_save_directory.js | 114 + .../browser_local_files_no_save_without_asking.js | 61 + .../browser_local_files_open_doesnt_duplicate.js | 121 + .../browser_open_internal_choice_persistence.js | 310 ++ .../tests/mochitest/browser_pdf_save_as.js | 121 + .../tests/mochitest/browser_protocol_ask_dialog.js | 464 +++ .../browser_protocol_ask_dialog_external.js | 199 ++ .../browser_protocol_ask_dialog_permission.js | 1348 +++++++ .../mochitest/browser_protocol_custom_sandbox.js | 140 + .../browser_protocol_custom_sandbox_csp.js | 38 + .../mochitest/browser_protocolhandler_loop.js | 74 + .../mochitest/browser_remember_download_option.js | 61 + .../tests/mochitest/browser_save_filenames.js | 823 +++++ .../browser_shows_where_to_save_dialog.js | 303 ++ .../mochitest/browser_txt_download_save_as.js | 168 + .../browser_web_handler_app_pinned_tab.js | 81 + .../mochitest/browser_web_protocol_handlers.js | 124 + uriloader/exthandler/tests/mochitest/download.bin | 1 + uriloader/exthandler/tests/mochitest/download.sjs | 42 + .../exthandler/tests/mochitest/download_page.html | 22 + uriloader/exthandler/tests/mochitest/file_as.exe | 1 + .../tests/mochitest/file_as.exe^headers^ | 2 + .../mochitest/file_external_protocol_iframe.html | 1 + .../exthandler/tests/mochitest/file_green.webp | Bin 0 -> 42 bytes .../tests/mochitest/file_green.webp^headers^ | 3 + .../tests/mochitest/file_image_svgxml.svg | 3 + .../tests/mochitest/file_image_svgxml.svg^headers^ | 2 + .../mochitest/file_nested_protocol_request.html | 1 + .../file_pdf_application_octet_stream.pdf | 0 .../file_pdf_application_octet_stream.pdf^headers^ | 2 + .../tests/mochitest/file_pdf_application_pdf.pdf | 0 .../file_pdf_application_pdf.pdf^headers^ | 2 + .../mochitest/file_pdf_application_unknown.pdf | 0 .../file_pdf_application_unknown.pdf^headers^ | 2 + .../mochitest/file_pdf_binary_octet_stream.pdf | 0 .../file_pdf_binary_octet_stream.pdf^headers^ | 2 + .../tests/mochitest/file_txt_attachment_test.txt | 0 .../file_txt_attachment_test.txt^headers^ | 2 + .../tests/mochitest/file_with@@funny_name.png | Bin 0 -> 1991 bytes .../mochitest/file_with@@funny_name.png^headers^ | 2 + .../tests/mochitest/file_with[funny_name.webm | Bin 0 -> 512 bytes .../mochitest/file_with[funny_name.webm^headers^ | 2 + .../file_xml_attachment_binary_octet_stream.xml | 4 + ...xml_attachment_binary_octet_stream.xml^headers^ | 2 + .../tests/mochitest/file_xml_attachment_test.xml | 4 + .../file_xml_attachment_test.xml^headers^ | 2 + uriloader/exthandler/tests/mochitest/head.js | 535 +++ .../tests/mochitest/invalidCharFileExtension.sjs | 18 + uriloader/exthandler/tests/mochitest/mailto.html | 11 + .../tests/mochitest/mime_type_download.sjs | 21 + uriloader/exthandler/tests/mochitest/mochitest.ini | 17 + .../tests/mochitest/protocolHandler.html | 16 + .../mochitest/protocol_custom_sandbox_helper.sjs | 30 + .../exthandler/tests/mochitest/redirect_helper.sjs | 30 + .../exthandler/tests/mochitest/save_filenames.html | 360 ++ .../tests/mochitest/script_redirect.html | 5 + .../mochitest/test_invalidCharFileExtension.xhtml | 65 + .../tests/mochitest/test_nullCharFile.xhtml | 67 + .../tests/mochitest/test_spammy_page.html | 27 + .../test_unknown_ext_protocol_handlers.html | 28 + .../tests/mochitest/test_unsafeBidiChars.xhtml | 89 + .../tests/mochitest/unsafeBidiFileName.sjs | 18 + uriloader/exthandler/tests/moz.build | 35 + uriloader/exthandler/tests/unit/handlers.json | 83 + uriloader/exthandler/tests/unit/head.js | 82 + uriloader/exthandler/tests/unit/mailcap | 2 + .../exthandler/tests/unit/test_badMIMEType.js | 29 + .../tests/unit/test_defaults_handlerService.js | 159 + .../unit/test_downloads_improvements_migration.js | 244 ++ .../tests/unit/test_filename_sanitize.js | 398 +++ .../tests/unit/test_getFromTypeAndExtension.js | 17 + .../exthandler/tests/unit/test_getMIMEInfo_pdf.js | 30 + .../unit/test_getMIMEInfo_unknown_mime_type.js | 32 + ...est_getTypeFromExtension_ext_to_type_mapping.js | 65 + ...getTypeFromExtension_with_empty_Content_Type.js | 218 ++ .../exthandler/tests/unit/test_handlerService.js | 467 +++ .../tests/unit/test_handlerService_store.js | 752 ++++ .../exthandler/tests/unit/test_punycodeURIs.js | 126 + uriloader/exthandler/tests/unit/xpcshell.ini | 38 + .../exthandler/uikit/nsLocalHandlerAppUIKit.h | 27 + .../exthandler/uikit/nsLocalHandlerAppUIKit.mm | 16 + uriloader/exthandler/uikit/nsMIMEInfoUIKit.h | 31 + uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm | 12 + uriloader/exthandler/uikit/nsOSHelperAppService.h | 54 + uriloader/exthandler/uikit/nsOSHelperAppService.mm | 53 + uriloader/exthandler/unix/nsGNOMERegistry.cpp | 101 + uriloader/exthandler/unix/nsGNOMERegistry.h | 28 + uriloader/exthandler/unix/nsMIMEInfoUnix.cpp | 84 + uriloader/exthandler/unix/nsMIMEInfoUnix.h | 30 + uriloader/exthandler/unix/nsOSHelperAppService.cpp | 1498 ++++++++ uriloader/exthandler/unix/nsOSHelperAppService.h | 122 + uriloader/exthandler/win/nsMIMEInfoWin.cpp | 913 +++++ uriloader/exthandler/win/nsMIMEInfoWin.h | 79 + uriloader/exthandler/win/nsOSHelperAppService.cpp | 612 ++++ uriloader/exthandler/win/nsOSHelperAppService.h | 81 + uriloader/moz.build | 20 + uriloader/prefetch/moz.build | 26 + uriloader/prefetch/nsIPrefetchService.idl | 54 + uriloader/prefetch/nsPrefetchService.cpp | 895 +++++ uriloader/prefetch/nsPrefetchService.h | 130 + uriloader/preload/FetchPreloader.cpp | 353 ++ uriloader/preload/FetchPreloader.h | 101 + uriloader/preload/PreloadHashKey.cpp | 213 ++ uriloader/preload/PreloadHashKey.h | 109 + uriloader/preload/PreloadService.cpp | 296 ++ uriloader/preload/PreloadService.h | 135 + uriloader/preload/PreloaderBase.cpp | 391 ++ uriloader/preload/PreloaderBase.h | 200 ++ uriloader/preload/gtest/TestFetchPreloader.cpp | 950 +++++ uriloader/preload/gtest/moz.build | 18 + uriloader/preload/moz.build | 26 + 201 files changed, 36354 insertions(+) create mode 100644 uriloader/base/moz.build create mode 100644 uriloader/base/nsCURILoader.idl create mode 100644 uriloader/base/nsDocLoader.cpp create mode 100644 uriloader/base/nsDocLoader.h create mode 100644 uriloader/base/nsIContentHandler.idl create mode 100644 uriloader/base/nsIDocumentLoader.idl create mode 100644 uriloader/base/nsITransfer.idl create mode 100644 uriloader/base/nsIURIContentListener.idl create mode 100644 uriloader/base/nsIURILoader.idl create mode 100644 uriloader/base/nsIWebProgress.idl create mode 100644 uriloader/base/nsIWebProgressListener.idl create mode 100644 uriloader/base/nsIWebProgressListener2.idl create mode 100644 uriloader/base/nsURILoader.cpp create mode 100644 uriloader/base/nsURILoader.h create mode 100644 uriloader/docs/index.rst create mode 100644 uriloader/docs/uriloader.rst create mode 100644 uriloader/exthandler/ContentHandlerService.cpp create mode 100644 uriloader/exthandler/ContentHandlerService.h create mode 100644 uriloader/exthandler/DBusHelpers.h create mode 100644 uriloader/exthandler/ExtHandlerService.sys.mjs create mode 100644 uriloader/exthandler/ExternalHelperAppChild.cpp create mode 100644 uriloader/exthandler/ExternalHelperAppChild.h create mode 100644 uriloader/exthandler/ExternalHelperAppParent.cpp create mode 100644 uriloader/exthandler/ExternalHelperAppParent.h create mode 100644 uriloader/exthandler/HandlerList.sys.mjs create mode 100644 uriloader/exthandler/HandlerServiceChild.h create mode 100644 uriloader/exthandler/HandlerServiceParent.cpp create mode 100644 uriloader/exthandler/HandlerServiceParent.h create mode 100644 uriloader/exthandler/PExternalHelperApp.ipdl create mode 100644 uriloader/exthandler/PHandlerService.ipdl create mode 100644 uriloader/exthandler/WebHandlerApp.sys.mjs create mode 100644 uriloader/exthandler/android/nsMIMEInfoAndroid.cpp create mode 100644 uriloader/exthandler/android/nsMIMEInfoAndroid.h create mode 100644 uriloader/exthandler/android/nsOSHelperAppService.cpp create mode 100644 uriloader/exthandler/android/nsOSHelperAppService.h create mode 100644 uriloader/exthandler/components.conf create mode 100644 uriloader/exthandler/docs/index.rst create mode 100644 uriloader/exthandler/mac/nsDecodeAppleFile.cpp create mode 100644 uriloader/exthandler/mac/nsDecodeAppleFile.h create mode 100644 uriloader/exthandler/mac/nsLocalHandlerAppMac.h create mode 100644 uriloader/exthandler/mac/nsLocalHandlerAppMac.mm create mode 100644 uriloader/exthandler/mac/nsMIMEInfoMac.h create mode 100644 uriloader/exthandler/mac/nsMIMEInfoMac.mm create mode 100644 uriloader/exthandler/mac/nsOSHelperAppService.h create mode 100644 uriloader/exthandler/mac/nsOSHelperAppService.mm create mode 100644 uriloader/exthandler/moz.build create mode 100644 uriloader/exthandler/nsCExternalHandlerService.idl create mode 100644 uriloader/exthandler/nsContentHandlerApp.h create mode 100644 uriloader/exthandler/nsDBusHandlerApp.cpp create mode 100644 uriloader/exthandler/nsDBusHandlerApp.h create mode 100644 uriloader/exthandler/nsExternalHelperAppService.cpp create mode 100644 uriloader/exthandler/nsExternalHelperAppService.h create mode 100644 uriloader/exthandler/nsExternalProtocolHandler.cpp create mode 100644 uriloader/exthandler/nsExternalProtocolHandler.h create mode 100644 uriloader/exthandler/nsIContentDispatchChooser.idl create mode 100644 uriloader/exthandler/nsIExternalHelperAppService.idl create mode 100644 uriloader/exthandler/nsIExternalProtocolService.idl create mode 100644 uriloader/exthandler/nsIHandlerService.idl create mode 100644 uriloader/exthandler/nsIHelperAppLauncherDialog.idl create mode 100644 uriloader/exthandler/nsISharingHandlerApp.idl create mode 100644 uriloader/exthandler/nsLocalHandlerApp.cpp create mode 100644 uriloader/exthandler/nsLocalHandlerApp.h create mode 100644 uriloader/exthandler/nsMIMEInfoChild.h create mode 100644 uriloader/exthandler/nsMIMEInfoImpl.cpp create mode 100644 uriloader/exthandler/nsMIMEInfoImpl.h create mode 100644 uriloader/exthandler/nsOSHelperAppServiceChild.cpp create mode 100644 uriloader/exthandler/nsOSHelperAppServiceChild.h create mode 100644 uriloader/exthandler/tests/HandlerServiceTestUtils.sys.mjs create mode 100644 uriloader/exthandler/tests/WriteArgument.cpp create mode 100644 uriloader/exthandler/tests/gtest/ExternalHelperAppServiceTest.cpp create mode 100644 uriloader/exthandler/tests/gtest/moz.build create mode 100644 uriloader/exthandler/tests/mochitest/FTPprotocolHandler.html create mode 100644 uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js create mode 100644 uriloader/exthandler/tests/mochitest/blank.html create mode 100644 uriloader/exthandler/tests/mochitest/browser.ini create mode 100644 uriloader/exthandler/tests/mochitest/browser_auto_close_window.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_auto_close_window_nodialog.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_bad_download_dir.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_download_idn_blocklist.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_download_preferred_action.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_download_skips_dialog.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_download_spam_permissions.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_download_urlescape.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_extension_correction.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_filehandling_loop.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_ftp_protocol_handlers.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_launched_app_save_directory.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_local_files_no_save_without_asking.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_local_files_open_doesnt_duplicate.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_pdf_save_as.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_external.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox_csp.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_remember_download_option.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_save_filenames.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_shows_where_to_save_dialog.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_txt_download_save_as.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_web_handler_app_pinned_tab.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js create mode 100644 uriloader/exthandler/tests/mochitest/download.bin create mode 100644 uriloader/exthandler/tests/mochitest/download.sjs create mode 100644 uriloader/exthandler/tests/mochitest/download_page.html create mode 100644 uriloader/exthandler/tests/mochitest/file_as.exe create mode 100644 uriloader/exthandler/tests/mochitest/file_as.exe^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html create mode 100644 uriloader/exthandler/tests/mochitest/file_green.webp create mode 100644 uriloader/exthandler/tests/mochitest/file_green.webp^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_image_svgxml.svg create mode 100644 uriloader/exthandler/tests/mochitest/file_image_svgxml.svg^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html create mode 100644 uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf create mode 100644 uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf create mode 100644 uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf create mode 100644 uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf create mode 100644 uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt create mode 100644 uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_with@@funny_name.png create mode 100644 uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_with[funny_name.webm create mode 100644 uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml create mode 100644 uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml create mode 100644 uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ create mode 100644 uriloader/exthandler/tests/mochitest/head.js create mode 100644 uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs create mode 100644 uriloader/exthandler/tests/mochitest/mailto.html create mode 100644 uriloader/exthandler/tests/mochitest/mime_type_download.sjs create mode 100644 uriloader/exthandler/tests/mochitest/mochitest.ini create mode 100644 uriloader/exthandler/tests/mochitest/protocolHandler.html create mode 100644 uriloader/exthandler/tests/mochitest/protocol_custom_sandbox_helper.sjs create mode 100644 uriloader/exthandler/tests/mochitest/redirect_helper.sjs create mode 100644 uriloader/exthandler/tests/mochitest/save_filenames.html create mode 100644 uriloader/exthandler/tests/mochitest/script_redirect.html create mode 100644 uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml create mode 100644 uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml create mode 100644 uriloader/exthandler/tests/mochitest/test_spammy_page.html create mode 100644 uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html create mode 100644 uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml create mode 100644 uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs create mode 100644 uriloader/exthandler/tests/moz.build create mode 100644 uriloader/exthandler/tests/unit/handlers.json create mode 100644 uriloader/exthandler/tests/unit/head.js create mode 100644 uriloader/exthandler/tests/unit/mailcap create mode 100644 uriloader/exthandler/tests/unit/test_badMIMEType.js create mode 100644 uriloader/exthandler/tests/unit/test_defaults_handlerService.js create mode 100644 uriloader/exthandler/tests/unit/test_downloads_improvements_migration.js create mode 100644 uriloader/exthandler/tests/unit/test_filename_sanitize.js create mode 100644 uriloader/exthandler/tests/unit/test_getFromTypeAndExtension.js create mode 100644 uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js create mode 100644 uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js create mode 100644 uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js create mode 100644 uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js create mode 100644 uriloader/exthandler/tests/unit/test_handlerService.js create mode 100644 uriloader/exthandler/tests/unit/test_handlerService_store.js create mode 100644 uriloader/exthandler/tests/unit/test_punycodeURIs.js create mode 100644 uriloader/exthandler/tests/unit/xpcshell.ini create mode 100644 uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h create mode 100644 uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm create mode 100644 uriloader/exthandler/uikit/nsMIMEInfoUIKit.h create mode 100644 uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm create mode 100644 uriloader/exthandler/uikit/nsOSHelperAppService.h create mode 100644 uriloader/exthandler/uikit/nsOSHelperAppService.mm create mode 100644 uriloader/exthandler/unix/nsGNOMERegistry.cpp create mode 100644 uriloader/exthandler/unix/nsGNOMERegistry.h create mode 100644 uriloader/exthandler/unix/nsMIMEInfoUnix.cpp create mode 100644 uriloader/exthandler/unix/nsMIMEInfoUnix.h create mode 100644 uriloader/exthandler/unix/nsOSHelperAppService.cpp create mode 100644 uriloader/exthandler/unix/nsOSHelperAppService.h create mode 100644 uriloader/exthandler/win/nsMIMEInfoWin.cpp create mode 100644 uriloader/exthandler/win/nsMIMEInfoWin.h create mode 100644 uriloader/exthandler/win/nsOSHelperAppService.cpp create mode 100644 uriloader/exthandler/win/nsOSHelperAppService.h create mode 100644 uriloader/moz.build create mode 100644 uriloader/prefetch/moz.build create mode 100644 uriloader/prefetch/nsIPrefetchService.idl create mode 100644 uriloader/prefetch/nsPrefetchService.cpp create mode 100644 uriloader/prefetch/nsPrefetchService.h create mode 100644 uriloader/preload/FetchPreloader.cpp create mode 100644 uriloader/preload/FetchPreloader.h create mode 100644 uriloader/preload/PreloadHashKey.cpp create mode 100644 uriloader/preload/PreloadHashKey.h create mode 100644 uriloader/preload/PreloadService.cpp create mode 100644 uriloader/preload/PreloadService.h create mode 100644 uriloader/preload/PreloaderBase.cpp create mode 100644 uriloader/preload/PreloaderBase.h create mode 100644 uriloader/preload/gtest/TestFetchPreloader.cpp create mode 100644 uriloader/preload/gtest/moz.build create mode 100644 uriloader/preload/moz.build (limited to 'uriloader') diff --git a/uriloader/base/moz.build b/uriloader/base/moz.build new file mode 100644 index 0000000000..36282f6f14 --- /dev/null +++ b/uriloader/base/moz.build @@ -0,0 +1,43 @@ +# -*- 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/. + +include("/ipc/chromium/chromium-config.mozbuild") + +with Files("**"): + BUG_COMPONENT = ("Core", "Document Navigation") + +with Files("nsITransfer.idl"): + BUG_COMPONENT = ("Firefox", "File Handling") + +XPIDL_SOURCES += [ + "nsCURILoader.idl", + "nsIContentHandler.idl", + "nsIDocumentLoader.idl", + "nsITransfer.idl", + "nsIURIContentListener.idl", + "nsIURILoader.idl", + "nsIWebProgress.idl", + "nsIWebProgressListener.idl", + "nsIWebProgressListener2.idl", +] + +XPIDL_MODULE = "uriloader" + +EXPORTS += [ + "nsDocLoader.h", + "nsURILoader.h", +] + +UNIFIED_SOURCES += [ + "nsDocLoader.cpp", + "nsURILoader.cpp", +] + +LOCAL_INCLUDES += [ + "/netwerk/base", +] + +FINAL_LIBRARY = "xul" diff --git a/uriloader/base/nsCURILoader.idl b/uriloader/base/nsCURILoader.idl new file mode 100644 index 0000000000..82f17fc83e --- /dev/null +++ b/uriloader/base/nsCURILoader.idl @@ -0,0 +1,18 @@ +/* -*- Mode: IDL; tab-width: 3; 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 "nsIURILoader.idl" + +/* +nsCURILoader implements: +------------------------- +nsIURILoader +*/ + +%{ C++ +#define NS_CONTENT_HANDLER_CONTRACTID "@mozilla.org/uriloader/content-handler;1" +#define NS_CONTENT_HANDLER_CONTRACTID_PREFIX NS_CONTENT_HANDLER_CONTRACTID "?type=" +%} diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp new file mode 100644 index 0000000000..e1e46ccdce --- /dev/null +++ b/uriloader/base/nsDocLoader.cpp @@ -0,0 +1,1612 @@ +/* -*- 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 "nspr.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Components.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Logging.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/PresShell.h" + +#include "nsDocLoader.h" +#include "nsDocShell.h" +#include "nsLoadGroup.h" +#include "nsNetUtil.h" +#include "nsIHttpChannel.h" +#include "nsIWebNavigation.h" +#include "nsIWebProgressListener2.h" + +#include "nsString.h" + +#include "nsCOMPtr.h" +#include "nscore.h" +#include "nsIWeakReferenceUtils.h" +#include "nsQueryObject.h" + +#include "nsPIDOMWindow.h" +#include "nsGlobalWindow.h" + +#include "nsIStringBundle.h" + +#include "nsIDocShell.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocGroup.h" +#include "nsPresContext.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIBrowserDOMWindow.h" +#include "nsGlobalWindow.h" +#include "mozilla/ThrottledEventQueue.h" +using namespace mozilla; +using mozilla::DebugOnly; +using mozilla::eLoad; +using mozilla::EventDispatcher; +using mozilla::LogLevel; +using mozilla::WidgetEvent; +using mozilla::dom::BrowserChild; +using mozilla::dom::BrowsingContext; +using mozilla::dom::Document; + +// +// Log module for nsIDocumentLoader logging... +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=DocLoader:5 +// set MOZ_LOG_FILE=debug.log +// +// this enables LogLevel::Debug level information and places all output in +// the file 'debug.log'. +// +mozilla::LazyLogModule gDocLoaderLog("DocLoader"); + +#if defined(DEBUG) +void GetURIStringFromRequest(nsIRequest* request, nsACString& name) { + if (request) + request->GetName(name); + else + name.AssignLiteral("???"); +} +#endif /* DEBUG */ + +void nsDocLoader::RequestInfoHashInitEntry(PLDHashEntryHdr* entry, + const void* key) { + // Initialize the entry with placement new + new (entry) nsRequestInfo(key); +} + +void nsDocLoader::RequestInfoHashClearEntry(PLDHashTable* table, + PLDHashEntryHdr* entry) { + nsRequestInfo* info = static_cast(entry); + info->~nsRequestInfo(); +} + +// this is used for mListenerInfoList.Contains() +template <> +class nsDefaultComparator { + public: + bool Equals(const nsDocLoader::nsListenerInfo& aInfo, + nsIWebProgressListener* const& aListener) const { + nsCOMPtr listener = + do_QueryReferent(aInfo.mWeakListener); + return aListener == listener; + } +}; + +/* static */ const PLDHashTableOps nsDocLoader::sRequestInfoHashOps = { + PLDHashTable::HashVoidPtrKeyStub, PLDHashTable::MatchEntryStub, + PLDHashTable::MoveEntryStub, nsDocLoader::RequestInfoHashClearEntry, + nsDocLoader::RequestInfoHashInitEntry}; + +nsDocLoader::nsDocLoader(bool aNotifyAboutBackgroundRequests) + : mParent(nullptr), + mProgressStateFlags(0), + mCurrentSelfProgress(0), + mMaxSelfProgress(0), + mCurrentTotalProgress(0), + mMaxTotalProgress(0), + mRequestInfoHash(&sRequestInfoHashOps, sizeof(nsRequestInfo)), + mCompletedTotalProgress(0), + mIsLoadingDocument(false), + mIsRestoringDocument(false), + mDontFlushLayout(false), + mIsFlushingLayout(false), + mTreatAsBackgroundLoad(false), + mHasFakeOnLoadDispatched(false), + mIsReadyToHandlePostMessage(false), + mDocumentOpenedButNotLoaded(false), + mNotifyAboutBackgroundRequests(aNotifyAboutBackgroundRequests) { + ClearInternalProgress(); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, ("DocLoader:%p: created.\n", this)); +} + +nsresult nsDocLoader::SetDocLoaderParent(nsDocLoader* aParent) { + mParent = aParent; + return NS_OK; +} + +nsresult nsDocLoader::Init() { + RefPtr loadGroup = new net::nsLoadGroup(); + nsresult rv = loadGroup->Init(); + if (NS_FAILED(rv)) return rv; + + loadGroup->SetGroupObserver(this, mNotifyAboutBackgroundRequests); + + mLoadGroup = loadGroup; + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: load group %p.\n", this, mLoadGroup.get())); + + return NS_OK; +} + +nsresult nsDocLoader::InitWithBrowsingContext( + BrowsingContext* aBrowsingContext) { + RefPtr loadGroup = new net::nsLoadGroup(); + if (!aBrowsingContext->GetRequestContextId()) { + return NS_ERROR_NOT_AVAILABLE; + } + nsresult rv = loadGroup->InitWithRequestContextId( + aBrowsingContext->GetRequestContextId()); + if (NS_FAILED(rv)) return rv; + + loadGroup->SetGroupObserver(this, mNotifyAboutBackgroundRequests); + + mLoadGroup = loadGroup; + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: load group %p.\n", this, mLoadGroup.get())); + + return NS_OK; +} + +nsDocLoader::~nsDocLoader() { + /* + |ClearWeakReferences()| here is intended to prevent people holding + weak references from re-entering this destructor since |QueryReferent()| + will |AddRef()| me, and the subsequent |Release()| will try to destroy me. + At this point there should be only weak references remaining (otherwise, we + wouldn't be getting destroyed). + + An alternative would be incrementing our refcount (consider it a + compressed flag saying "Don't re-destroy."). I haven't yet decided which + is better. [scc] + */ + // XXXbz now that NS_IMPL_RELEASE stabilizes by setting refcount to 1, is + // this needed? + ClearWeakReferences(); + + Destroy(); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, ("DocLoader:%p: deleted.\n", this)); +} + +/* + * Implementation of ISupports methods... + */ +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsDocLoader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsDocLoader) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDocLoader) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDocumentLoader) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIDocumentLoader) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIWebProgress) + NS_INTERFACE_MAP_ENTRY(nsIProgressEventSink) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsIChannelEventSink) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsDocLoader) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WEAK(nsDocLoader, mChildrenInOnload) + +/* + * Implementation of nsIInterfaceRequestor methods... + */ +NS_IMETHODIMP nsDocLoader::GetInterface(const nsIID& aIID, void** aSink) { + nsresult rv = NS_ERROR_NO_INTERFACE; + + NS_ENSURE_ARG_POINTER(aSink); + + if (aIID.Equals(NS_GET_IID(nsILoadGroup))) { + *aSink = mLoadGroup; + NS_IF_ADDREF((nsISupports*)*aSink); + rv = NS_OK; + } else { + rv = QueryInterface(aIID, aSink); + } + + return rv; +} + +/* static */ +already_AddRefed nsDocLoader::GetAsDocLoader( + nsISupports* aSupports) { + RefPtr ret = do_QueryObject(aSupports); + return ret.forget(); +} + +/* static */ +nsresult nsDocLoader::AddDocLoaderAsChildOfRoot(nsDocLoader* aDocLoader) { + nsCOMPtr docLoaderService = + components::DocLoader::Service(); + NS_ENSURE_TRUE(docLoaderService, NS_ERROR_UNEXPECTED); + + RefPtr rootDocLoader = GetAsDocLoader(docLoaderService); + NS_ENSURE_TRUE(rootDocLoader, NS_ERROR_UNEXPECTED); + + return rootDocLoader->AddChildLoader(aDocLoader); +} + +// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsDocLoader::Stop(void) { + nsresult rv = NS_OK; + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Stop() called\n", this)); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, Stop, ()); + + if (mLoadGroup) { + rv = mLoadGroup->CancelWithReason(NS_BINDING_ABORTED, + "nsDocLoader::Stop"_ns); + } + + // Don't report that we're flushing layout so IsBusy returns false after a + // Stop call. + mIsFlushingLayout = false; + + // Clear out mChildrenInOnload. We're not going to fire our onload + // anyway at this point, and there's no issue with mChildrenInOnload + // after this, since mDocumentRequest will be null after the + // DocLoaderIsEmpty() call. + mChildrenInOnload.Clear(); + nsCOMPtr ds = do_QueryInterface(GetAsSupports(this)); + Document* doc = ds ? ds->GetExtantDocument() : nullptr; + if (doc) { + doc->ClearOOPChildrenLoading(); + } + + // Make sure to call DocLoaderIsEmpty now so that we reset mDocumentRequest, + // etc, as needed. We could be getting into here from a subframe onload, in + // which case the call to DocLoaderIsEmpty() is coming but hasn't quite + // happened yet, Canceling the loadgroup did nothing (because it was already + // empty), and we're about to start a new load (which is what triggered this + // Stop() call). + + // XXXbz If the child frame loadgroups were requests in mLoadgroup, I suspect + // we wouldn't need the call here.... + + NS_ASSERTION(!IsBusy(), "Shouldn't be busy here"); + + // If Cancelling the load group only had pending subresource requests, then + // the group status will still be success, and we would fire the load event. + // We want to avoid that when we're aborting the load, so override the status + // with an explicit NS_BINDING_ABORTED value. + DocLoaderIsEmpty(false, Some(NS_BINDING_ABORTED)); + + return rv; +} + +bool nsDocLoader::TreatAsBackgroundLoad() { return mTreatAsBackgroundLoad; } + +void nsDocLoader::SetBackgroundLoadIframe() { mTreatAsBackgroundLoad = true; } + +bool nsDocLoader::IsBusy() { + nsresult rv; + + // + // A document loader is busy if either: + // + // 1. One of its children is in the middle of an onload handler. Note that + // the handler may have already removed this child from mChildList! + // 2. It is currently loading a document and either has parts of it still + // loading, or has a busy child docloader. + // 3. It's currently flushing layout in DocLoaderIsEmpty(). + // + + nsCOMPtr ds = do_QueryInterface(GetAsSupports(this)); + Document* doc = ds ? ds->GetExtantDocument() : nullptr; + if (!mChildrenInOnload.IsEmpty() || (doc && doc->HasOOPChildrenLoading()) || + mIsFlushingLayout) { + return true; + } + + /* Is this document loader busy? */ + if (!IsBlockingLoadEvent()) { + return false; + } + + // Check if any in-process sub-document is awaiting its 'load' event: + bool busy; + rv = mLoadGroup->IsPending(&busy); + if (NS_FAILED(rv)) { + return false; + } + if (busy) { + return true; + } + + /* check its child document loaders... */ + uint32_t count = mChildList.Length(); + for (uint32_t i = 0; i < count; i++) { + nsIDocumentLoader* loader = ChildAt(i); + + // If 'dom.cross_origin_iframes_loaded_in_background' is set, the parent + // document treats cross domain iframes as background loading frame + if (loader && static_cast(loader)->TreatAsBackgroundLoad()) { + continue; + } + // This is a safe cast, because we only put nsDocLoader objects into the + // array + if (loader && static_cast(loader)->IsBusy()) return true; + } + + return false; +} + +NS_IMETHODIMP +nsDocLoader::GetContainer(nsISupports** aResult) { + NS_ADDREF(*aResult = static_cast(this)); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetLoadGroup(nsILoadGroup** aResult) { + nsresult rv = NS_OK; + + if (nullptr == aResult) { + rv = NS_ERROR_NULL_POINTER; + } else { + *aResult = mLoadGroup; + NS_IF_ADDREF(*aResult); + } + return rv; +} + +void nsDocLoader::Destroy() { + Stop(); + + // Remove the document loader from the parent list of loaders... + if (mParent) { + DebugOnly rv = mParent->RemoveChildLoader(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "RemoveChildLoader failed"); + } + + // Release all the information about network requests... + ClearRequestInfoHash(); + + mListenerInfoList.Clear(); + mListenerInfoList.Compact(); + + mDocumentRequest = nullptr; + + if (mLoadGroup) mLoadGroup->SetGroupObserver(nullptr); + + DestroyChildren(); +} + +void nsDocLoader::DestroyChildren() { + uint32_t count = mChildList.Length(); + // if the doc loader still has children...we need to enumerate the + // children and make them null out their back ptr to the parent doc + // loader + for (uint32_t i = 0; i < count; i++) { + nsIDocumentLoader* loader = ChildAt(i); + + if (loader) { + // This is a safe cast, as we only put nsDocLoader objects into the + // array + DebugOnly rv = + static_cast(loader)->SetDocLoaderParent(nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SetDocLoaderParent failed"); + } + } + mChildList.Clear(); +} + +NS_IMETHODIMP +nsDocLoader::OnStartRequest(nsIRequest* request) { + // called each time a request is added to the group. + + // Some docloaders deal with background requests in their OnStartRequest + // override, but here we don't want to do anything with them, so return early. + nsLoadFlags loadFlags = 0; + request->GetLoadFlags(&loadFlags); + if (loadFlags & nsIRequest::LOAD_BACKGROUND) { + return NS_OK; + } + + if (MOZ_LOG_TEST(gDocLoaderLog, LogLevel::Debug)) { + nsAutoCString name; + request->GetName(name); + + uint32_t count = 0; + if (mLoadGroup) mLoadGroup->GetActiveCount(&count); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: OnStartRequest[%p](%s) mIsLoadingDocument=%s, %u " + "active URLs", + this, request, name.get(), (mIsLoadingDocument ? "true" : "false"), + count)); + } + + bool justStartedLoading = false; + + if (!mIsLoadingDocument && (loadFlags & nsIChannel::LOAD_DOCUMENT_URI)) { + justStartedLoading = true; + mIsLoadingDocument = true; + mDocumentOpenedButNotLoaded = false; + ClearInternalProgress(); // only clear our progress if we are starting a + // new load.... + } + + // + // Create a new nsRequestInfo for the request that is starting to + // load... + // + AddRequestInfo(request); + + // + // Only fire a doStartDocumentLoad(...) if the document loader + // has initiated a load... Otherwise, this notification has + // resulted from a request being added to the load group. + // + if (mIsLoadingDocument) { + if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) { + // + // Make sure that the document channel is null at this point... + // (unless its been redirected) + // + NS_ASSERTION( + (loadFlags & nsIChannel::LOAD_REPLACE) || !(mDocumentRequest.get()), + "Overwriting an existing document channel!"); + + // This request is associated with the entire document... + mDocumentRequest = request; + mLoadGroup->SetDefaultLoadRequest(request); + + // Only fire the start document load notification for the first + // document URI... Do not fire it again for redirections + // + if (justStartedLoading) { + // Update the progress status state + mProgressStateFlags = nsIWebProgressListener::STATE_START; + + // Fire the start document load notification + doStartDocumentLoad(); + return NS_OK; + } + } + } + + NS_ASSERTION(!mIsLoadingDocument || mDocumentRequest, + "mDocumentRequest MUST be set for the duration of a page load!"); + + // This is the only way to catch document request start event after a redirect + // has occured without changing inherited Firefox behaviour significantly. + // Problem description: + // The combination of |STATE_START + STATE_IS_DOCUMENT| is only sent for + // initial request (see |doStartDocumentLoad| call above). + // And |STATE_REDIRECTING + STATE_IS_DOCUMENT| is sent with old channel, which + // makes it impossible to filter by destination URL (see + // |AsyncOnChannelRedirect| implementation). + // Fixing any of those bugs may cause unpredictable consequences in any part + // of the browser, so we just add a custom flag for this exact situation. + int32_t extraFlags = 0; + if (mIsLoadingDocument && !justStartedLoading && + (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) && + (loadFlags & nsIChannel::LOAD_REPLACE)) { + extraFlags = nsIWebProgressListener::STATE_IS_REDIRECTED_DOCUMENT; + } + doStartURLLoad(request, extraFlags); + + return NS_OK; +} + +// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +nsDocLoader::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + // Some docloaders deal with background requests in their OnStopRequest + // override, but here we don't want to do anything with them, so return early. + nsLoadFlags lf = 0; + aRequest->GetLoadFlags(&lf); + if (lf & nsIRequest::LOAD_BACKGROUND) { + return NS_OK; + } + + nsresult rv = NS_OK; + + if (MOZ_LOG_TEST(gDocLoaderLog, LogLevel::Debug)) { + nsAutoCString name; + aRequest->GetName(name); + + uint32_t count = 0; + if (mLoadGroup) mLoadGroup->GetActiveCount(&count); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: OnStopRequest[%p](%s) status=%" PRIx32 + " mIsLoadingDocument=%s, mDocumentOpenedButNotLoaded=%s," + " %u active URLs", + this, aRequest, name.get(), static_cast(aStatus), + (mIsLoadingDocument ? "true" : "false"), + (mDocumentOpenedButNotLoaded ? "true" : "false"), count)); + } + + bool fireTransferring = false; + + // + // Set the Maximum progress to the same value as the current progress. + // Since the URI has finished loading, all the data is there. Also, + // this will allow a more accurate estimation of the max progress (in case + // the old value was unknown ie. -1) + // + nsRequestInfo* info = GetRequestInfo(aRequest); + if (info) { + // Null out mLastStatus now so we don't find it when looking for + // status from now on. This destroys the nsStatusInfo and hence + // removes it from our list. + info->mLastStatus = nullptr; + + int64_t oldMax = info->mMaxProgress; + + info->mMaxProgress = info->mCurrentProgress; + + // + // If a request whose content-length was previously unknown has just + // finished loading, then use this new data to try to calculate a + // mMaxSelfProgress... + // + if ((oldMax < int64_t(0)) && (mMaxSelfProgress < int64_t(0))) { + mMaxSelfProgress = CalculateMaxProgress(); + } + + // As we know the total progress of this request now, save it to be part + // of CalculateMaxProgress() result. We need to remove the info from the + // hash, see bug 480713. + mCompletedTotalProgress += info->mMaxProgress; + + // + // Determine whether a STATE_TRANSFERRING notification should be + // 'synthesized'. + // + // If nsRequestInfo::mMaxProgress (as stored in oldMax) and + // nsRequestInfo::mCurrentProgress are both 0, then the + // STATE_TRANSFERRING notification has not been fired yet... + // + if ((oldMax == 0) && (info->mCurrentProgress == 0)) { + nsCOMPtr channel(do_QueryInterface(aRequest)); + + // Only fire a TRANSFERRING notification if the request is also a + // channel -- data transfer requires a nsIChannel! + // + if (channel) { + if (NS_SUCCEEDED(aStatus)) { + fireTransferring = true; + } + // + // If the request failed (for any reason other than being + // redirected or retargeted), the TRANSFERRING notification can + // still be fired if a HTTP connection was established to a server. + // + else if (aStatus != NS_BINDING_REDIRECTED && + aStatus != NS_BINDING_RETARGETED) { + // + // Only if the load has been targeted (see bug 268483)... + // + if (lf & nsIChannel::LOAD_TARGETED) { + nsCOMPtr httpChannel(do_QueryInterface(aRequest)); + if (httpChannel) { + uint32_t responseCode; + rv = httpChannel->GetResponseStatus(&responseCode); + if (NS_SUCCEEDED(rv)) { + // + // A valid server status indicates that a connection was + // established to the server... So, fire the notification + // even though a failure occurred later... + // + fireTransferring = true; + } + } + } + } + } + } + } + + if (fireTransferring) { + // Send a STATE_TRANSFERRING notification for the request. + int32_t flags; + + flags = nsIWebProgressListener::STATE_TRANSFERRING | + nsIWebProgressListener::STATE_IS_REQUEST; + // + // Move the WebProgress into the STATE_TRANSFERRING state if necessary... + // + if (mProgressStateFlags & nsIWebProgressListener::STATE_START) { + mProgressStateFlags = nsIWebProgressListener::STATE_TRANSFERRING; + + // Send STATE_TRANSFERRING for the document too... + flags |= nsIWebProgressListener::STATE_IS_DOCUMENT; + } + + FireOnStateChange(this, aRequest, flags, NS_OK); + } + + // + // Fire the OnStateChange(...) notification for stop request + // + doStopURLLoad(aRequest, aStatus); + + // Clear this request out of the hash to avoid bypass of FireOnStateChange + // when address of the request is reused. + RemoveRequestInfo(aRequest); + + // For the special case where the current document is an initial about:blank + // document, we may still have subframes loading, and keeping the DocLoader + // busy. In that case, if we have an error, we won't show it until those + // frames finish loading, which is nonsensical. So stop any subframe loads + // now. + if (NS_FAILED(aStatus) && aStatus != NS_BINDING_ABORTED && + aStatus != NS_BINDING_REDIRECTED && aStatus != NS_BINDING_RETARGETED) { + if (RefPtr doc = do_GetInterface(GetAsSupports(this))) { + if (doc->IsInitialDocument()) { + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, Stop, ()); + } + } + } + + // + // Only fire the DocLoaderIsEmpty(...) if we may need to fire onload. + // + if (IsBlockingLoadEvent()) { + nsCOMPtr ds = + do_QueryInterface(static_cast(this)); + bool doNotFlushLayout = false; + if (ds) { + // Don't do unexpected layout flushes while we're in process of restoring + // a document from the bfcache. + ds->GetRestoringDocument(&doNotFlushLayout); + } + DocLoaderIsEmpty(!doNotFlushLayout); + } + + return NS_OK; +} + +nsresult nsDocLoader::RemoveChildLoader(nsDocLoader* aChild) { + nsresult rv = mChildList.RemoveElement(aChild) ? NS_OK : NS_ERROR_FAILURE; + if (NS_SUCCEEDED(rv)) { + rv = aChild->SetDocLoaderParent(nullptr); + } + return rv; +} + +nsresult nsDocLoader::AddChildLoader(nsDocLoader* aChild) { + mChildList.AppendElement(aChild); + return aChild->SetDocLoaderParent(this); +} + +NS_IMETHODIMP nsDocLoader::GetDocumentChannel(nsIChannel** aChannel) { + if (!mDocumentRequest) { + *aChannel = nullptr; + return NS_OK; + } + + return CallQueryInterface(mDocumentRequest, aChannel); +} + +void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout, + const Maybe& aOverrideStatus) { + if (IsBlockingLoadEvent()) { + /* In the unimagineably rude circumstance that onload event handlers + triggered by this function actually kill the window ... ok, it's + not unimagineable; it's happened ... this deathgrip keeps this object + alive long enough to survive this function call. */ + nsCOMPtr kungFuDeathGrip(this); + + // Don't flush layout if we're still busy. + if (IsBusy()) { + return; + } + + NS_ASSERTION(!mIsFlushingLayout, "Someone screwed up"); + // We may not have a document request if we are in a + // document.open() situation. + NS_ASSERTION(mDocumentRequest || mDocumentOpenedButNotLoaded, + "No Document Request!"); + + // The load group for this DocumentLoader is idle. Flush if we need to. + if (aFlushLayout && !mDontFlushLayout) { + nsCOMPtr doc = do_GetInterface(GetAsSupports(this)); + if (doc) { + // We start loads from style resolution, so we need to flush out style + // no matter what. If we have user fonts, we also need to flush layout, + // since the reflow is what starts font loads. + mozilla::FlushType flushType = mozilla::FlushType::Style; + // Be safe in case this presshell is in teardown now + doc->FlushUserFontSet(); + if (doc->GetUserFontSet()) { + flushType = mozilla::FlushType::Layout; + } + mDontFlushLayout = mIsFlushingLayout = true; + doc->FlushPendingNotifications(flushType); + mDontFlushLayout = mIsFlushingLayout = false; + } + } + + // And now check whether we're really busy; that might have changed with + // the layout flush. + // + // Note, mDocumentRequest can be null while mDocumentOpenedButNotLoaded is + // false if the flushing above re-entered this method. + if (IsBusy() || (!mDocumentRequest && !mDocumentOpenedButNotLoaded)) { + return; + } + + if (mDocumentRequest) { + // Clear out our request info hash, now that our load really is done and + // we don't need it anymore to CalculateMaxProgress(). + ClearInternalProgress(); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Is now idle...\n", this)); + + nsCOMPtr docRequest = mDocumentRequest; + + mDocumentRequest = nullptr; + mIsLoadingDocument = false; + + // Update the progress status state - the document is done + mProgressStateFlags = nsIWebProgressListener::STATE_STOP; + + nsresult loadGroupStatus = NS_OK; + if (aOverrideStatus) { + loadGroupStatus = *aOverrideStatus; + } else { + mLoadGroup->GetStatus(&loadGroupStatus); + } + + // + // New code to break the circular reference between + // the load group and the docloader... + // + mLoadGroup->SetDefaultLoadRequest(nullptr); + + // Take a ref to our parent now so that we can call ChildDoneWithOnload() + // on it even if our onload handler removes us from the docloader tree. + RefPtr parent = mParent; + + // Note that if calling ChildEnteringOnload() on the parent returns false + // then calling our onload handler is not safe. That can only happen on + // OOM, so that's ok. + if (!parent || parent->ChildEnteringOnload(this)) { + // Do nothing with our state after firing the + // OnEndDocumentLoad(...). The document loader may be loading a *new* + // document - if LoadDocument() was called from a handler! + // + doStopDocumentLoad(docRequest, loadGroupStatus); + + NotifyDoneWithOnload(parent); + } + } else { + MOZ_ASSERT(mDocumentOpenedButNotLoaded); + mDocumentOpenedButNotLoaded = false; + + // Make sure we do the ChildEnteringOnload/ChildDoneWithOnload even if we + // plan to skip firing our own load event, because otherwise we might + // never end up firing our parent's load event. + RefPtr parent = mParent; + if (!parent || parent->ChildEnteringOnload(this)) { + nsresult loadGroupStatus = NS_OK; + mLoadGroup->GetStatus(&loadGroupStatus); + // Make sure we're not canceling the loadgroup. If we are, then just + // like the normal navigation case we should not fire a load event. + if (NS_SUCCEEDED(loadGroupStatus) || + loadGroupStatus == NS_ERROR_PARSED_DATA_CACHED) { + // Can "doc" or "window" ever come back null here? Our state machine + // is complicated enough I wouldn't bet against it... + nsCOMPtr doc = do_GetInterface(GetAsSupports(this)); + if (doc) { + doc->SetReadyStateInternal(Document::READYSTATE_COMPLETE, + /* updateTimingInformation = */ false); + doc->StopDocumentLoad(); + + nsCOMPtr window = doc->GetWindow(); + if (window && !doc->SkipLoadEventAfterClose()) { + if (!mozilla::dom::DocGroup::TryToLoadIframesInBackground() || + (mozilla::dom::DocGroup::TryToLoadIframesInBackground() && + !HasFakeOnLoadDispatched())) { + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Firing load event for document.open\n", + this)); + + // This is a very cut-down version of + // nsDocumentViewer::LoadComplete that doesn't do various things + // that are not relevant here because this wasn't an actual + // navigation. + WidgetEvent event(true, eLoad); + event.mFlags.mBubbles = false; + event.mFlags.mCancelable = false; + // Dispatching to |window|, but using |document| as the target, + // per spec. + event.mTarget = doc; + nsEventStatus unused = nsEventStatus_eIgnore; + doc->SetLoadEventFiring(true); + EventDispatcher::Dispatch(window, nullptr, &event, nullptr, + &unused); + doc->SetLoadEventFiring(false); + + // Now unsuppress painting on the presshell, if we + // haven't done that yet. + RefPtr presShell = doc->GetPresShell(); + if (presShell && !presShell->IsDestroying()) { + presShell->UnsuppressPainting(); + + if (!presShell->IsDestroying()) { + presShell->LoadComplete(); + } + } + } + } + } + } + NotifyDoneWithOnload(parent); + } + } + } +} + +void nsDocLoader::NotifyDoneWithOnload(nsDocLoader* aParent) { + if (aParent) { + // In-process parent: + aParent->ChildDoneWithOnload(this); + } + nsCOMPtr docShell = do_QueryInterface(this); + if (!docShell) { + return; + } + BrowsingContext* bc = nsDocShell::Cast(docShell)->GetBrowsingContext(); + if (bc->IsContentSubframe() && !bc->GetParent()->IsInProcess()) { + if (BrowserChild* browserChild = BrowserChild::GetFrom(docShell)) { + mozilla::Unused << browserChild->SendMaybeFireEmbedderLoadEvents( + dom::EmbedderElementEventType::NoEvent); + } + } +} + +void nsDocLoader::doStartDocumentLoad(void) { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(mDocumentRequest, buffer); + MOZ_LOG( + gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange for start document load (...)." + "\tURI: %s \n", + this, buffer.get())); +#endif /* DEBUG */ + + // Fire an OnStatus(...) notification STATE_START. This indicates + // that the document represented by mDocumentRequest has started to + // load... + FireOnStateChange(this, mDocumentRequest, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_DOCUMENT | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_WINDOW | + nsIWebProgressListener::STATE_IS_NETWORK, + NS_OK); +} + +void nsDocLoader::doStartURLLoad(nsIRequest* request, int32_t aExtraFlags) { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange start url load (...)." + "\tURI: %s\n", + this, buffer.get())); +#endif /* DEBUG */ + + FireOnStateChange(this, request, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_REQUEST | aExtraFlags, + NS_OK); +} + +void nsDocLoader::doStopURLLoad(nsIRequest* request, nsresult aStatus) { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange for end url load (...)." + "\tURI: %s status=%" PRIx32 "\n", + this, buffer.get(), static_cast(aStatus))); +#endif /* DEBUG */ + + FireOnStateChange(this, request, + nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_REQUEST, + aStatus); + + // Fire a status change message for the most recent unfinished + // request to make sure that the displayed status is not outdated. + if (!mStatusInfoList.isEmpty()) { + nsStatusInfo* statusInfo = mStatusInfoList.getFirst(); + FireOnStatusChange(this, statusInfo->mRequest, statusInfo->mStatusCode, + statusInfo->mStatusMessage.get()); + } +} + +void nsDocLoader::doStopDocumentLoad(nsIRequest* request, nsresult aStatus) { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange for end document load (...)." + "\tURI: %s Status=%" PRIx32 "\n", + this, buffer.get(), static_cast(aStatus))); +#endif /* DEBUG */ + + // Firing STATE_STOP|STATE_IS_DOCUMENT will fire onload handlers. + // Grab our parent chain before doing that so we can still dispatch + // STATE_STOP|STATE_IS_WINDW_STATE_IS_NETWORK to them all, even if + // the onload handlers rearrange the docshell tree. + WebProgressList list; + GatherAncestorWebProgresses(list); + + // + // Fire an OnStateChange(...) notification indicating the the + // current document has finished loading... + // + int32_t flags = nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_DOCUMENT; + for (uint32_t i = 0; i < list.Length(); ++i) { + list[i]->DoFireOnStateChange(this, request, flags, aStatus); + } + + // + // Fire a final OnStateChange(...) notification indicating the the + // current document has finished loading... + // + flags = nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_WINDOW | + nsIWebProgressListener::STATE_IS_NETWORK; + for (uint32_t i = 0; i < list.Length(); ++i) { + list[i]->DoFireOnStateChange(this, request, flags, aStatus); + } +} + +//////////////////////////////////////////////////////////////////////////////////// +// The following section contains support for nsIWebProgress and related stuff +//////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsDocLoader::AddProgressListener(nsIWebProgressListener* aListener, + uint32_t aNotifyMask) { + if (mListenerInfoList.Contains(aListener)) { + // The listener is already registered! + return NS_ERROR_FAILURE; + } + + nsWeakPtr listener = do_GetWeakReference(aListener); + if (!listener) { + return NS_ERROR_INVALID_ARG; + } + + mListenerInfoList.AppendElement(nsListenerInfo(listener, aNotifyMask)); + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::RemoveProgressListener(nsIWebProgressListener* aListener) { + return mListenerInfoList.RemoveElement(aListener) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsDocLoader::GetBrowsingContextXPCOM(BrowsingContext** aResult) { + *aResult = nullptr; + return NS_OK; +} + +BrowsingContext* nsDocLoader::GetBrowsingContext() { return nullptr; } + +NS_IMETHODIMP +nsDocLoader::GetDOMWindow(mozIDOMWindowProxy** aResult) { + return CallGetInterface(this, aResult); +} + +NS_IMETHODIMP +nsDocLoader::GetIsTopLevel(bool* aResult) { + nsCOMPtr docShell = do_QueryInterface(this); + *aResult = docShell && docShell->GetBrowsingContext()->IsTop(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetIsLoadingDocument(bool* aIsLoadingDocument) { + *aIsLoadingDocument = mIsLoadingDocument; + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetLoadType(uint32_t* aLoadType) { + *aLoadType = 0; + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocLoader::GetTarget(nsIEventTarget** aTarget) { + nsCOMPtr window; + nsresult rv = GetDOMWindow(getter_AddRefs(window)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr global = do_QueryInterface(window); + NS_ENSURE_STATE(global); + + nsCOMPtr target = + global->EventTargetFor(mozilla::TaskCategory::Other); + target.forget(aTarget); + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::SetTarget(nsIEventTarget* aTarget) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +int64_t nsDocLoader::GetMaxTotalProgress() { + int64_t newMaxTotal = 0; + + uint32_t count = mChildList.Length(); + for (uint32_t i = 0; i < count; i++) { + int64_t individualProgress = 0; + nsIDocumentLoader* docloader = ChildAt(i); + if (docloader) { + // Cast is safe since all children are nsDocLoader too + individualProgress = ((nsDocLoader*)docloader)->GetMaxTotalProgress(); + } + if (individualProgress < int64_t(0)) // if one of the elements doesn't know + // it's size then none of them do + { + newMaxTotal = int64_t(-1); + break; + } else + newMaxTotal += individualProgress; + } + + int64_t progress = -1; + if (mMaxSelfProgress >= int64_t(0) && newMaxTotal >= int64_t(0)) + progress = newMaxTotal + mMaxSelfProgress; + + return progress; +} + +//////////////////////////////////////////////////////////////////////////////////// +// The following section contains support for nsIProgressEventSink which is used +// to pass progress and status between the actual request and the doc loader. +// The doc loader then turns around and makes the right web progress calls based +// on this information. +//////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsDocLoader::OnProgress(nsIRequest* aRequest, int64_t aProgress, + int64_t aProgressMax) { + int64_t progressDelta = 0; + + // + // Update the RequestInfo entry with the new progress data + // + if (nsRequestInfo* info = GetRequestInfo(aRequest)) { + // Update info->mCurrentProgress before we call FireOnStateChange, + // since that can make the "info" pointer invalid. + int64_t oldCurrentProgress = info->mCurrentProgress; + progressDelta = aProgress - oldCurrentProgress; + info->mCurrentProgress = aProgress; + + // suppress sending STATE_TRANSFERRING if this is upload progress (see bug + // 240053) + if (!info->mUploading && (int64_t(0) == oldCurrentProgress) && + (int64_t(0) == info->mMaxProgress)) { + // + // If we receive an OnProgress event from a toplevel channel that the URI + // Loader has not yet targeted, then we must suppress the event. This is + // necessary to ensure that webprogresslisteners do not get confused when + // the channel is finally targeted. See bug 257308. + // + nsLoadFlags lf = 0; + aRequest->GetLoadFlags(&lf); + if ((lf & nsIChannel::LOAD_DOCUMENT_URI) && + !(lf & nsIChannel::LOAD_TARGETED)) { + MOZ_LOG( + gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p Ignoring OnProgress while load is not targeted\n", + this)); + return NS_OK; + } + + // + // This is the first progress notification for the entry. If + // (aMaxProgress != -1) then the content-length of the data is known, + // so update mMaxSelfProgress... Otherwise, set it to -1 to indicate + // that the content-length is no longer known. + // + if (aProgressMax != -1) { + mMaxSelfProgress += aProgressMax; + info->mMaxProgress = aProgressMax; + } else { + mMaxSelfProgress = int64_t(-1); + info->mMaxProgress = int64_t(-1); + } + + // Send a STATE_TRANSFERRING notification for the request. + int32_t flags; + + flags = nsIWebProgressListener::STATE_TRANSFERRING | + nsIWebProgressListener::STATE_IS_REQUEST; + // + // Move the WebProgress into the STATE_TRANSFERRING state if necessary... + // + if (mProgressStateFlags & nsIWebProgressListener::STATE_START) { + mProgressStateFlags = nsIWebProgressListener::STATE_TRANSFERRING; + + // Send STATE_TRANSFERRING for the document too... + flags |= nsIWebProgressListener::STATE_IS_DOCUMENT; + } + + FireOnStateChange(this, aRequest, flags, NS_OK); + } + + // Update our overall current progress count. + mCurrentSelfProgress += progressDelta; + } + // + // The request is not part of the load group, so ignore its progress + // information... + // + else { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(aRequest, buffer); + MOZ_LOG( + gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p OOPS - No Request Info for: %s\n", this, buffer.get())); +#endif /* DEBUG */ + + return NS_OK; + } + + // + // Fire progress notifications out to any registered nsIWebProgressListeners + // + FireOnProgressChange(this, aRequest, aProgress, aProgressMax, progressDelta, + mCurrentTotalProgress, mMaxTotalProgress); + + return NS_OK; +} + +NS_IMETHODIMP nsDocLoader::OnStatus(nsIRequest* aRequest, nsresult aStatus, + const char16_t* aStatusArg) { + // + // Fire progress notifications out to any registered nsIWebProgressListeners + // + if (aStatus != NS_OK) { + // Remember the current status for this request + nsRequestInfo* info; + info = GetRequestInfo(aRequest); + if (info) { + bool uploading = (aStatus == NS_NET_STATUS_WRITING || + aStatus == NS_NET_STATUS_SENDING_TO); + // If switching from uploading to downloading (or vice versa), then we + // need to reset our progress counts. This is designed with HTTP form + // submission in mind, where an upload is performed followed by download + // of possibly several documents. + if (info->mUploading != uploading) { + mCurrentSelfProgress = mMaxSelfProgress = 0; + mCurrentTotalProgress = mMaxTotalProgress = 0; + mCompletedTotalProgress = 0; + info->mUploading = uploading; + info->mCurrentProgress = 0; + info->mMaxProgress = 0; + } + } + + nsCOMPtr sbs = + mozilla::components::StringBundle::Service(); + if (!sbs) return NS_ERROR_FAILURE; + nsAutoString msg; + nsresult rv = sbs->FormatStatusMessage(aStatus, aStatusArg, msg); + if (NS_FAILED(rv)) return rv; + + // Keep around the message. In case a request finishes, we need to make sure + // to send the status message of another request to our user to that we + // don't display, for example, "Transferring" messages for requests that are + // already done. + if (info) { + if (!info->mLastStatus) { + info->mLastStatus = MakeUnique(aRequest); + } else { + // We're going to move it to the front of the list, so remove + // it from wherever it is now. + info->mLastStatus->remove(); + } + info->mLastStatus->mStatusMessage = msg; + info->mLastStatus->mStatusCode = aStatus; + // Put the info at the front of the list + mStatusInfoList.insertFront(info->mLastStatus.get()); + } + FireOnStatusChange(this, aRequest, aStatus, msg.get()); + } + return NS_OK; +} + +void nsDocLoader::ClearInternalProgress() { + ClearRequestInfoHash(); + + mCurrentSelfProgress = mMaxSelfProgress = 0; + mCurrentTotalProgress = mMaxTotalProgress = 0; + mCompletedTotalProgress = 0; + + mProgressStateFlags = nsIWebProgressListener::STATE_STOP; +} + +/** + * |_code| is executed for every listener matching |_flag| + * |listener| should be used inside |_code| as the nsIWebProgressListener var. + */ +#define NOTIFY_LISTENERS(_flag, _code) \ + PR_BEGIN_MACRO \ + nsCOMPtr listener; \ + ListenerArray::BackwardIterator iter(mListenerInfoList); \ + while (iter.HasMore()) { \ + nsListenerInfo& info = iter.GetNext(); \ + if (!(info.mNotifyMask & (_flag))) { \ + continue; \ + } \ + listener = do_QueryReferent(info.mWeakListener); \ + if (!listener) { \ + iter.Remove(); \ + continue; \ + } \ + _code \ + } \ + mListenerInfoList.Compact(); \ + PR_END_MACRO + +void nsDocLoader::FireOnProgressChange(nsDocLoader* aLoadInitiator, + nsIRequest* request, int64_t aProgress, + int64_t aProgressMax, + int64_t aProgressDelta, + int64_t aTotalProgress, + int64_t aMaxTotalProgress) { + if (mIsLoadingDocument) { + mCurrentTotalProgress += aProgressDelta; + mMaxTotalProgress = GetMaxTotalProgress(); + + aTotalProgress = mCurrentTotalProgress; + aMaxTotalProgress = mMaxTotalProgress; + } + +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Progress (%s): curSelf: %" PRId64 " maxSelf: %" PRId64 + " curTotal: %" PRId64 " maxTotal %" PRId64 "\n", + this, buffer.get(), aProgress, aProgressMax, aTotalProgress, + aMaxTotalProgress)); +#endif /* DEBUG */ + + NOTIFY_LISTENERS( + nsIWebProgress::NOTIFY_PROGRESS, + // XXX truncates 64-bit to 32-bit + listener->OnProgressChange(aLoadInitiator, request, int32_t(aProgress), + int32_t(aProgressMax), int32_t(aTotalProgress), + int32_t(aMaxTotalProgress));); + + // Pass the notification up to the parent... + if (mParent) { + mParent->FireOnProgressChange(aLoadInitiator, request, aProgress, + aProgressMax, aProgressDelta, aTotalProgress, + aMaxTotalProgress); + } +} + +void nsDocLoader::GatherAncestorWebProgresses(WebProgressList& aList) { + for (nsDocLoader* loader = this; loader; loader = loader->mParent) { + aList.AppendElement(loader); + } +} + +void nsDocLoader::FireOnStateChange(nsIWebProgress* aProgress, + nsIRequest* aRequest, int32_t aStateFlags, + nsresult aStatus) { + WebProgressList list; + GatherAncestorWebProgresses(list); + for (uint32_t i = 0; i < list.Length(); ++i) { + list[i]->DoFireOnStateChange(aProgress, aRequest, aStateFlags, aStatus); + } +} + +void nsDocLoader::DoFireOnStateChange(nsIWebProgress* const aProgress, + nsIRequest* const aRequest, + int32_t& aStateFlags, + const nsresult aStatus) { + // + // Remove the STATE_IS_NETWORK bit if necessary. + // + // The rule is to remove this bit, if the notification has been passed + // up from a child WebProgress, and the current WebProgress is already + // active... + // + if (mIsLoadingDocument && + (aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK) && + (this != aProgress)) { + aStateFlags &= ~nsIWebProgressListener::STATE_IS_NETWORK; + } + + // Add the STATE_RESTORING bit if necessary. + if (mIsRestoringDocument) + aStateFlags |= nsIWebProgressListener::STATE_RESTORING; + +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(aRequest, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Status (%s): code: %x\n", this, buffer.get(), + aStateFlags)); +#endif /* DEBUG */ + + NS_ASSERTION(aRequest, + "Firing OnStateChange(...) notification with a NULL request!"); + + NOTIFY_LISTENERS( + ((aStateFlags >> 16) & nsIWebProgress::NOTIFY_STATE_ALL), + listener->OnStateChange(aProgress, aRequest, aStateFlags, aStatus);); +} + +void nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* aUri, + uint32_t aFlags) { + NOTIFY_LISTENERS( + nsIWebProgress::NOTIFY_LOCATION, + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader [%p] calling %p->OnLocationChange to %s %x", this, + listener.get(), aUri->GetSpecOrDefault().get(), aFlags)); + listener->OnLocationChange(aWebProgress, aRequest, aUri, aFlags);); + + // Pass the notification up to the parent... + if (mParent) { + mParent->FireOnLocationChange(aWebProgress, aRequest, aUri, aFlags); + } +} + +void nsDocLoader::FireOnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + NOTIFY_LISTENERS( + nsIWebProgress::NOTIFY_STATUS, + listener->OnStatusChange(aWebProgress, aRequest, aStatus, aMessage);); + + // Pass the notification up to the parent... + if (mParent) { + mParent->FireOnStatusChange(aWebProgress, aRequest, aStatus, aMessage); + } +} + +bool nsDocLoader::RefreshAttempted(nsIWebProgress* aWebProgress, nsIURI* aURI, + uint32_t aDelay, bool aSameURI) { + /* + * Returns true if the refresh may proceed, + * false if the refresh should be blocked. + */ + bool allowRefresh = true; + + NOTIFY_LISTENERS( + nsIWebProgress::NOTIFY_REFRESH, + nsCOMPtr listener2 = + do_QueryReferent(info.mWeakListener); + if (!listener2) continue; + + bool listenerAllowedRefresh; + nsresult listenerRV = listener2->OnRefreshAttempted( + aWebProgress, aURI, aDelay, aSameURI, &listenerAllowedRefresh); + if (NS_FAILED(listenerRV)) continue; + + allowRefresh = allowRefresh && listenerAllowedRefresh;); + + // Pass the notification up to the parent... + if (mParent) { + allowRefresh = allowRefresh && mParent->RefreshAttempted(aWebProgress, aURI, + aDelay, aSameURI); + } + + return allowRefresh; +} + +nsresult nsDocLoader::AddRequestInfo(nsIRequest* aRequest) { + if (!mRequestInfoHash.Add(aRequest, mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +void nsDocLoader::RemoveRequestInfo(nsIRequest* aRequest) { + mRequestInfoHash.Remove(aRequest); +} + +nsDocLoader::nsRequestInfo* nsDocLoader::GetRequestInfo( + nsIRequest* aRequest) const { + return static_cast(mRequestInfoHash.Search(aRequest)); +} + +void nsDocLoader::ClearRequestInfoHash(void) { mRequestInfoHash.Clear(); } + +int64_t nsDocLoader::CalculateMaxProgress() { + int64_t max = mCompletedTotalProgress; + for (auto iter = mRequestInfoHash.Iter(); !iter.Done(); iter.Next()) { + auto info = static_cast(iter.Get()); + + if (info->mMaxProgress < info->mCurrentProgress) { + return int64_t(-1); + } + max += info->mMaxProgress; + } + return max; +} + +NS_IMETHODIMP nsDocLoader::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* cb) { + if (aOldChannel) { + nsLoadFlags loadFlags = 0; + int32_t stateFlags = nsIWebProgressListener::STATE_REDIRECTING | + nsIWebProgressListener::STATE_IS_REQUEST; + + aOldChannel->GetLoadFlags(&loadFlags); + // If the document channel is being redirected, then indicate that the + // document is being redirected in the notification... + if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) { + stateFlags |= nsIWebProgressListener::STATE_IS_DOCUMENT; + +#if defined(DEBUG) + // We only set mDocumentRequest in OnStartRequest(), but its possible + // to get a redirect before that for service worker interception. + if (mDocumentRequest) { + nsCOMPtr request(aOldChannel); + NS_ASSERTION(request == mDocumentRequest, "Wrong Document Channel"); + } +#endif /* DEBUG */ + } + + OnRedirectStateChange(aOldChannel, aNewChannel, aFlags, stateFlags); + FireOnStateChange(this, aOldChannel, stateFlags, NS_OK); + } + + cb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +void nsDocLoader::OnSecurityChange(nsISupports* aContext, uint32_t aState) { + // + // Fire progress notifications out to any registered nsIWebProgressListeners. + // + + nsCOMPtr request = do_QueryInterface(aContext); + nsIWebProgress* webProgress = static_cast(this); + + NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_SECURITY, + listener->OnSecurityChange(webProgress, request, aState);); + + // Pass the notification up to the parent... + if (mParent) { + mParent->OnSecurityChange(aContext, aState); + } +} + +/* + * Implementation of nsISupportsPriority methods... + * + * The priority of the DocLoader _is_ the priority of its LoadGroup. + * + * XXX(darin): Once we start storing loadgroups in loadgroups, this code will + * go away. + */ + +NS_IMETHODIMP nsDocLoader::GetPriority(int32_t* aPriority) { + nsCOMPtr p = do_QueryInterface(mLoadGroup); + if (p) return p->GetPriority(aPriority); + + *aPriority = 0; + return NS_OK; +} + +NS_IMETHODIMP nsDocLoader::SetPriority(int32_t aPriority) { + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: SetPriority(%d) called\n", this, aPriority)); + + nsCOMPtr p = do_QueryInterface(mLoadGroup); + if (p) p->SetPriority(aPriority); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, SetPriority, + (aPriority)); + + return NS_OK; +} + +NS_IMETHODIMP nsDocLoader::AdjustPriority(int32_t aDelta) { + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: AdjustPriority(%d) called\n", this, aDelta)); + + nsCOMPtr p = do_QueryInterface(mLoadGroup); + if (p) p->AdjustPriority(aDelta); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, AdjustPriority, + (aDelta)); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetDocumentRequest(nsIRequest** aRequest) { + NS_IF_ADDREF(*aRequest = mDocumentRequest); + return NS_OK; +} + +#if 0 +void nsDocLoader::DumpChannelInfo() +{ + nsChannelInfo *info; + int32_t i, count; + int32_t current=0, max=0; + + + printf("==== DocLoader=%x\n", this); + + count = mChannelInfoList.Count(); + for(i=0; imURI) { + rv = info->mURI->GetSpec(buffer); + } + + printf(" [%d] current=%d max=%d [%s]\n", i, + info->mCurrentProgress, + info->mMaxProgress, buffer.get()); +# endif /* DEBUG */ + + current += info->mCurrentProgress; + if (max >= 0) { + if (info->mMaxProgress < info->mCurrentProgress) { + max = -1; + } else { + max += info->mMaxProgress; + } + } + } + + printf("\nCurrent=%d Total=%d\n====\n", current, max); +} +#endif /* 0 */ diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h new file mode 100644 index 0000000000..e828cb8a11 --- /dev/null +++ b/uriloader/base/nsDocLoader.h @@ -0,0 +1,390 @@ +/* -*- 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 nsDocLoader_h__ +#define nsDocLoader_h__ + +#include "nsIDocumentLoader.h" +#include "nsIWebProgress.h" +#include "nsIWebProgressListener.h" +#include "nsIRequestObserver.h" +#include "nsWeakReference.h" +#include "nsILoadGroup.h" +#include "nsCOMArray.h" +#include "nsTObserverArray.h" +#include "nsString.h" +#include "nsIChannel.h" +#include "nsIProgressEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIChannelEventSink.h" +#include "nsISupportsPriority.h" +#include "nsCOMPtr.h" +#include "PLDHashTable.h" +#include "nsCycleCollectionParticipant.h" + +#include "mozilla/LinkedList.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace dom { +class BrowsingContext; +} // namespace dom +} // namespace mozilla + +/**************************************************************************** + * nsDocLoader implementation... + ****************************************************************************/ + +#define NS_THIS_DOCLOADER_IMPL_CID \ + { /* b4ec8387-98aa-4c08-93b6-6d23069c06f2 */ \ + 0xb4ec8387, 0x98aa, 0x4c08, { \ + 0x93, 0xb6, 0x6d, 0x23, 0x06, 0x9c, 0x06, 0xf2 \ + } \ + } + +class nsDocLoader : public nsIDocumentLoader, + public nsIRequestObserver, + public nsSupportsWeakReference, + public nsIProgressEventSink, + public nsIWebProgress, + public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsISupportsPriority { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_THIS_DOCLOADER_IMPL_CID) + + nsDocLoader() : nsDocLoader(false) {} + + [[nodiscard]] virtual nsresult Init(); + [[nodiscard]] nsresult InitWithBrowsingContext( + mozilla::dom::BrowsingContext* aBrowsingContext); + + static already_AddRefed GetAsDocLoader(nsISupports* aSupports); + // Needed to deal with ambiguous inheritance from nsISupports... + static nsISupports* GetAsSupports(nsDocLoader* aDocLoader) { + return static_cast(aDocLoader); + } + + // Add aDocLoader as a child to the docloader service. + [[nodiscard]] static nsresult AddDocLoaderAsChildOfRoot( + nsDocLoader* aDocLoader); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsDocLoader, nsIDocumentLoader) + + NS_DECL_NSIDOCUMENTLOADER + + // nsIProgressEventSink + NS_DECL_NSIPROGRESSEVENTSINK + + // nsIRequestObserver methods: (for observing the load group) + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIWEBPROGRESS + + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSISUPPORTSPRIORITY; // semicolon for clang-format bug 1629756 + + // Implementation specific methods... + + // Remove aChild from our childlist. This nulls out the child's mParent + // pointer. + [[nodiscard]] nsresult RemoveChildLoader(nsDocLoader* aChild); + + // Add aChild to our child list. This will set aChild's mParent pointer to + // |this|. + [[nodiscard]] nsresult AddChildLoader(nsDocLoader* aChild); + nsDocLoader* GetParent() const { return mParent; } + + struct nsListenerInfo { + nsListenerInfo(nsIWeakReference* aListener, unsigned long aNotifyMask) + : mWeakListener(aListener), mNotifyMask(aNotifyMask) {} + + // Weak pointer for the nsIWebProgressListener... + nsWeakPtr mWeakListener; + + // Mask indicating which notifications the listener wants to receive. + unsigned long mNotifyMask; + }; + + /** + * Fired when a security change occurs due to page transitions, + * or end document load. This interface should be called by + * a security package (eg Netscape Personal Security Manager) + * to notify nsIWebProgressListeners that security state has + * changed. State flags are in nsIWebProgressListener.idl + */ + void OnSecurityChange(nsISupports* aContext, uint32_t aState); + + void SetDocumentOpenedButNotLoaded() { mDocumentOpenedButNotLoaded = true; } + + bool TreatAsBackgroundLoad(); + + void SetFakeOnLoadDispatched() { mHasFakeOnLoadDispatched = true; }; + + bool HasFakeOnLoadDispatched() { return mHasFakeOnLoadDispatched; }; + + void ResetToFirstLoad() { + mHasFakeOnLoadDispatched = false; + mIsReadyToHandlePostMessage = false; + mTreatAsBackgroundLoad = false; + }; + + uint32_t ChildCount() const { return mChildList.Length(); } + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void OOPChildrenLoadingIsEmpty() { + DocLoaderIsEmpty(true); + } + + protected: + explicit nsDocLoader(bool aNotifyAboutBackgroundRequests); + virtual ~nsDocLoader(); + + [[nodiscard]] virtual nsresult SetDocLoaderParent(nsDocLoader* aLoader); + + bool IsBusy(); + + void SetBackgroundLoadIframe(); + + void Destroy(); + virtual void DestroyChildren(); + + nsIDocumentLoader* ChildAt(int32_t i) { + return mChildList.SafeElementAt(i, nullptr); + } + + void FireOnProgressChange(nsDocLoader* aLoadInitiator, nsIRequest* request, + int64_t aProgress, int64_t aProgressMax, + int64_t aProgressDelta, int64_t aTotalProgress, + int64_t aMaxTotalProgress); + + // This should be at least 2 long since we'll generally always + // have the current page and the global docloader on the ancestor + // list. But to deal with frames it's better to make it a bit + // longer, and it's always a stack temporary so there's no real + // reason not to. + typedef AutoTArray, 8> WebProgressList; + void GatherAncestorWebProgresses(WebProgressList& aList); + + void FireOnStateChange(nsIWebProgress* aProgress, nsIRequest* request, + int32_t aStateFlags, nsresult aStatus); + + // The guts of FireOnStateChange, but does not call itself on our ancestors. + // The arguments that are const are const so that we can detect cases when + // DoFireOnStateChange wants to propagate changes to the next web progress + // at compile time. The ones that are not, are references so that such + // changes can be propagated. + void DoFireOnStateChange(nsIWebProgress* const aProgress, + nsIRequest* const request, int32_t& aStateFlags, + const nsresult aStatus); + + void FireOnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsresult aStatus, const char16_t* aMessage); + + void FireOnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsIURI* aUri, uint32_t aFlags); + + [[nodiscard]] bool RefreshAttempted(nsIWebProgress* aWebProgress, + nsIURI* aURI, uint32_t aDelay, + bool aSameURI); + + // this function is overridden by the docshell, it is provided so that we + // can pass more information about redirect state (the normal OnStateChange + // doesn't get the new channel). + // @param aRedirectFlags The flags being sent to OnStateChange that + // indicate the type of redirect. + // @param aStateFlags The channel flags normally sent to OnStateChange. + virtual void OnRedirectStateChange(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, + uint32_t aRedirectFlags, + uint32_t aStateFlags) {} + + void doStartDocumentLoad(); + void doStartURLLoad(nsIRequest* request, int32_t aExtraFlags); + void doStopURLLoad(nsIRequest* request, nsresult aStatus); + void doStopDocumentLoad(nsIRequest* request, nsresult aStatus); + + void NotifyDoneWithOnload(nsDocLoader* aParent); + + // Inform a parent docloader that aChild is about to call its onload + // handler. + [[nodiscard]] bool ChildEnteringOnload(nsIDocumentLoader* aChild) { + // It's ok if we're already in the list -- we'll just be in there twice + // and then the RemoveObject calls from ChildDoneWithOnload will remove + // us. + return mChildrenInOnload.AppendObject(aChild); + } + + // Inform a parent docloader that aChild is done calling its onload + // handler. + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void ChildDoneWithOnload( + nsIDocumentLoader* aChild) { + mChildrenInOnload.RemoveObject(aChild); + DocLoaderIsEmpty(true); + } + + // DocLoaderIsEmpty should be called whenever the docloader may be empty. + // This method is idempotent and does nothing if the docloader is not in + // fact empty. This method _does_ make sure that layout is flushed if our + // loadgroup has no active requests before checking for "real" emptiness if + // aFlushLayout is true. + // @param aOverrideStatus An optional status to use when notifying listeners + // of the completed load, instead of using the load group's status. + MOZ_CAN_RUN_SCRIPT void DocLoaderIsEmpty( + bool aFlushLayout, + const mozilla::Maybe& aOverrideStatus = mozilla::Nothing()); + + protected: + struct nsStatusInfo : public mozilla::LinkedListElement { + nsString mStatusMessage; + nsresult mStatusCode; + // Weak mRequest is ok; we'll be told if it decides to go away. + nsIRequest* const mRequest; + + explicit nsStatusInfo(nsIRequest* aRequest) + : mStatusCode(NS_ERROR_NOT_INITIALIZED), mRequest(aRequest) { + MOZ_COUNT_CTOR(nsStatusInfo); + } + MOZ_COUNTED_DTOR(nsStatusInfo) + }; + + struct nsRequestInfo : public PLDHashEntryHdr { + explicit nsRequestInfo(const void* key) + : mKey(key), + mCurrentProgress(0), + mMaxProgress(0), + mUploading(false), + mLastStatus(nullptr) { + MOZ_COUNT_CTOR(nsRequestInfo); + } + + MOZ_COUNTED_DTOR(nsRequestInfo) + + nsIRequest* Request() { + return static_cast(const_cast(mKey)); + } + + const void* mKey; // Must be first for the PLDHashTable stubs to work + int64_t mCurrentProgress; + int64_t mMaxProgress; + bool mUploading; + + mozilla::UniquePtr mLastStatus; + }; + + static void RequestInfoHashInitEntry(PLDHashEntryHdr* entry, const void* key); + static void RequestInfoHashClearEntry(PLDHashTable* table, + PLDHashEntryHdr* entry); + + // IMPORTANT: The ownership implicit in the following member + // variables has been explicitly checked and set using nsCOMPtr + // for owning pointers and raw COM interface pointers for weak + // (ie, non owning) references. If you add any members to this + // class, please make the ownership explicit (pinkerton, scc). + + nsCOMPtr mDocumentRequest; // [OWNER] ???compare with document + + nsDocLoader* mParent; // [WEAK] + + typedef nsAutoTObserverArray ListenerArray; + ListenerArray mListenerInfoList; + + nsCOMPtr mLoadGroup; + // We hold weak refs to all our kids + nsTObserverArray mChildList; + + // The following member variables are related to the new nsIWebProgress + // feedback interfaces that travis cooked up. + int32_t mProgressStateFlags; + + int64_t mCurrentSelfProgress; + int64_t mMaxSelfProgress; + + int64_t mCurrentTotalProgress; + int64_t mMaxTotalProgress; + + PLDHashTable mRequestInfoHash; + int64_t mCompletedTotalProgress; + + mozilla::LinkedList mStatusInfoList; + + /* + * This flag indicates that the loader is loading a document. It is set + * from the call to LoadDocument(...) until the OnConnectionsComplete(...) + * notification is fired... + */ + bool mIsLoadingDocument; + + /* Flag to indicate that we're in the process of restoring a document. */ + bool mIsRestoringDocument; + + /* Flag to indicate that we're in the process of flushing layout + under DocLoaderIsEmpty() and should not do another flush. */ + bool mDontFlushLayout; + + /* Flag to indicate whether we should consider ourselves as currently + flushing layout for the purposes of IsBusy. For example, if Stop has + been called then IsBusy should return false even if we are still + flushing. */ + bool mIsFlushingLayout; + + bool mTreatAsBackgroundLoad; + + private: + bool mHasFakeOnLoadDispatched; + + bool mIsReadyToHandlePostMessage; + /** + * This flag indicates that the loader is waiting for completion of + * a document.open-triggered "document load". This is set when + * document.open() happens and sets up a new parser and cleared out + * when we go to fire our load event or end up with a new document + * channel. + */ + bool mDocumentOpenedButNotLoaded; + + bool mNotifyAboutBackgroundRequests; + + static const PLDHashTableOps sRequestInfoHashOps; + + // A list of kids that are in the middle of their onload calls and will let + // us know once they're done. We don't want to fire onload for "normal" + // DocLoaderIsEmpty calls (those coming from requests finishing in our + // loadgroup) unless this is empty. + nsCOMArray mChildrenInOnload; + + int64_t GetMaxTotalProgress(); + + nsresult AddRequestInfo(nsIRequest* aRequest); + void RemoveRequestInfo(nsIRequest* aRequest); + nsRequestInfo* GetRequestInfo(nsIRequest* aRequest) const; + void ClearRequestInfoHash(); + int64_t CalculateMaxProgress(); + /// void DumpChannelInfo(void); + + // used to clear our internal progress state between loads... + void ClearInternalProgress(); + + /** + * Used to test whether we might need to fire a load event. This + * can happen when we have a document load going on, or when we've + * had document.open() called and haven't fired the corresponding + * load event yet. + */ + bool IsBlockingLoadEvent() const { + return mIsLoadingDocument || mDocumentOpenedButNotLoaded; + } +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsDocLoader, NS_THIS_DOCLOADER_IMPL_CID) + +static inline nsISupports* ToSupports(nsDocLoader* aDocLoader) { + return static_cast(aDocLoader); +} + +#endif /* nsDocLoader_h__ */ diff --git a/uriloader/base/nsIContentHandler.idl b/uriloader/base/nsIContentHandler.idl new file mode 100644 index 0000000000..31ef87a8ba --- /dev/null +++ b/uriloader/base/nsIContentHandler.idl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; 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 nsIRequest; +interface nsIInterfaceRequestor; + +[scriptable, uuid(49439df2-b3d2-441c-bf62-866bdaf56fd2)] +interface nsIContentHandler : nsISupports +{ + /** + * Tells the content handler to take over handling the content. If this + * function succeeds, the URI Loader will leave this request alone, ignoring + * progress notifications. Failure of this method will cause the request to be + * cancelled, unless the error code is NS_ERROR_WONT_HANDLE_CONTENT (see + * below). + * + * @param aWindowContext + * Window context, used to get things like the current nsIDOMWindow + * for this request. May be null. + * @param aContentType + * The content type of aRequest + * @param aRequest + * A request whose content type is already known. + * + * @throw NS_ERROR_WONT_HANDLE_CONTENT Indicates that this handler does not + * want to handle this content. A different way for handling this + * content should be tried. + */ + void handleContent(in string aContentType, + in nsIInterfaceRequestor aWindowContext, + in nsIRequest aRequest); +}; diff --git a/uriloader/base/nsIDocumentLoader.idl b/uriloader/base/nsIDocumentLoader.idl new file mode 100644 index 0000000000..ae02a827e2 --- /dev/null +++ b/uriloader/base/nsIDocumentLoader.idl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; 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 nsILoadGroup; +interface nsIChannel; +interface nsIURI; +interface nsIWebProgress; +interface nsIRequest; + +/** + * An nsIDocumentLoader is an interface responsible for tracking groups of + * loads that belong together (images, external scripts, etc) and subdocuments + * (`; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URI); + + // Wait for the window and then click the link. + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog URL is as expected" + ); + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + + info("Removing tab to close the dialog."); + gBrowser.removeTab(tab); + await dialogClosedPromise; + ok(!dialog._frame.contentWindow, "The dialog should have been closed."); +}); + +/** + * Check that a cross-origin iframe can navigate the top frame + * to an external protocol. + */ +add_task(async function xorigin_iframe_can_navigate_top() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/" + ); + + // Ensure we notice the dialog opening: + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + let innerLoaded = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + "https://example.org/" + ); + info("Constructing frame"); + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + let frame = content.document.createElement("iframe"); + frame.src = "https://example.org/"; // cross-origin frame. + content.document.body.prepend(frame); + }); + await innerLoaded; + + info("Navigating top bc from frame"); + let parentBC = tab.linkedBrowser.browsingContext; + await SpecialPowers.spawn(parentBC.children[0], [], async function () { + content.eval("window.top.location.href = 'mailto:example@example.com';"); + }); + + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog opens as expected for navigating the top frame from an x-origin frame." + ); + // Close the dialog: + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + dialog.close(); + await dialogClosedPromise; + gBrowser.removeTab(tab); +}); + +/** + * Check that when navigating to an external protocol from an iframe in a + * background tab, we show the dialog in the correct tab. + */ +add_task(async function iframe_background_tab() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/" + ); + + let innerLoaded = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + "https://example.org/" + ); + info("Constructing frame"); + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + let frame = content.document.createElement("iframe"); + frame.src = "https://example.org/"; + content.document.body.prepend(frame); + }); + await innerLoaded; + + info("Switching to new tab"); + let newTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.net/" + ); + + // Wait for the chooser dialog to open in the background tab. It should not + // open in the foreground tab which is unrelated to the external protocol + // navigation. + let dialogWindowPromise = waitForProtocolAppChooserDialog(gBrowser, true); + + info("Navigating to external proto from frame in background tab"); + let parentBC = tab.linkedBrowser.browsingContext; + await SpecialPowers.spawn(parentBC.children[0], [], async function () { + content.eval("location.href = 'mailto:example@example.com';"); + }); + + // Wait for dialog to open in one of the tabs. + let dialog = await dialogWindowPromise; + + is( + gBrowser.getTabDialogBox(tab.linkedBrowser)._tabDialogManager._topDialog, + dialog, + "Dialog opened in the background tab" + ); + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Opened dialog is appChooser dialog." + ); + + // Close the dialog: + let dialogClosedPromise = waitForProtocolAppChooserDialog(gBrowser, false); + dialog.close(); + await dialogClosedPromise; + + gBrowser.removeTab(tab); + gBrowser.removeTab(newTab); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_external.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_external.js new file mode 100644 index 0000000000..591f1afbc5 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_external.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +/** + * Creates dummy protocol handler + */ +function initTestHandlers() { + let handlerInfoThatAsks = + HandlerServiceTestUtils.getBlankHandlerInfo("local-app-test"); + + let appHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + // This is a dir and not executable, but that's enough for here. + appHandler.executable = Services.dirsvc.get("XCurProcD", Ci.nsIFile); + handlerInfoThatAsks.possibleApplicationHandlers.appendElement(appHandler); + handlerInfoThatAsks.preferredApplicationHandler = appHandler; + handlerInfoThatAsks.preferredAction = handlerInfoThatAsks.useHelperApp; + handlerInfoThatAsks.alwaysAskBeforeHandling = false; + gHandlerService.store(handlerInfoThatAsks); + + let webHandlerInfo = + HandlerServiceTestUtils.getBlankHandlerInfo("web+somesite"); + let webHandler = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + webHandler.name = "Somesite"; + webHandler.uriTemplate = "https://example.com/handle_url?u=%s"; + webHandlerInfo.possibleApplicationHandlers.appendElement(webHandler); + webHandlerInfo.preferredApplicationHandler = webHandler; + webHandlerInfo.preferredAction = webHandlerInfo.useHelperApp; + webHandlerInfo.alwaysAskBeforeHandling = false; + gHandlerService.store(webHandlerInfo); + + registerCleanupFunction(() => { + gHandlerService.remove(webHandlerInfo); + gHandlerService.remove(handlerInfoThatAsks); + }); +} + +function makeCmdLineHelper(url) { + return Cu.createCommandLine( + ["-url", url], + null, + Ci.nsICommandLine.STATE_REMOTE_EXPLICIT + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["network.protocol-handler.prompt-from-external", true]], + }); + initTestHandlers(); +}); + +/** + * Check that if we get a direct request from another app / the OS to open a + * link, we always prompt, even if we think we know what the correct answer + * is. This is to avoid infinite loops in such situations where the OS and + * Firefox have conflicting ideas about the default handler, or where our + * checks with the OS don't work (Linux and/or Snap, at time of this comment). + */ +add_task(async function test_external_asks_anyway() { + let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService( + Ci.nsICommandLineHandler + ); + let chooserDialogOpenPromise = waitForProtocolAppChooserDialog( + gBrowser, + true + ); + let fakeCmdLine = makeCmdLineHelper("local-app-test:dummy"); + cmdLineHandler.handle(fakeCmdLine); + let dialog = await chooserDialogOpenPromise; + ok(dialog, "Should have prompted."); + + let dialogClosedPromise = waitForProtocolAppChooserDialog( + gBrowser.selectedBrowser, + false + ); + let dialogEl = dialog._frame.contentDocument.querySelector("dialog"); + dialogEl.cancelDialog(); + await dialogClosedPromise; + // We will have opened a tab; close it. + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Like the previous test, but avoid asking for web and extension handlers, + * as we can open those ourselves without looping. + */ +add_task(async function test_web_app_doesnt_ask() { + // Listen for a dialog open and fail the test if it does: + let dialogOpenListener = () => ok(false, "Shouldn't have opened a dialog!"); + document.documentElement.addEventListener("dialogopen", dialogOpenListener); + registerCleanupFunction(() => + document.documentElement.removeEventListener( + "dialogopen", + dialogOpenListener + ) + ); + + // Set up a promise for a tab to open with the right URL: + const kURL = "web+somesite:dummy"; + const kLoadedURL = + "https://example.com/handle_url?u=" + encodeURIComponent(kURL); + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, kLoadedURL); + + // Load the URL: + let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService( + Ci.nsICommandLineHandler + ); + let fakeCmdLine = makeCmdLineHelper(kURL); + cmdLineHandler.handle(fakeCmdLine); + + // Check that the tab loaded. If instead the dialog opened, the dialogopen handler + // will fail the test. + let tab = await tabPromise; + is( + tab.linkedBrowser.currentURI.spec, + kLoadedURL, + "Should have opened the right URL." + ); + BrowserTestUtils.removeTab(tab); + + // We do this both here and in cleanup so it's easy to add tasks to this test, + // and so we clean up correctly if the test aborts before we get here. + document.documentElement.removeEventListener( + "dialogopen", + dialogOpenListener + ); +}); + +add_task(async function external_https_redirect_doesnt_ask() { + Services.perms.addFromPrincipal( + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://example.com" + ), + "open-protocol-handler^local-app-test", + Services.perms.ALLOW_ACTION + ); + // Listen for a dialog open and fail the test if it does: + let dialogOpenListener = () => ok(false, "Shouldn't have opened a dialog!"); + document.documentElement.addEventListener("dialogopen", dialogOpenListener); + registerCleanupFunction(() => { + document.documentElement.removeEventListener( + "dialogopen", + dialogOpenListener + ); + Services.perms.removeAll(); + }); + + let initialTab = gBrowser.selectedTab; + + gHandlerService.wrappedJSObject.mockProtocolHandler("local-app-test"); + registerCleanupFunction(() => + gHandlerService.wrappedJSObject.mockProtocolHandler() + ); + + // Set up a promise for an app to have launched with the right URI: + let loadPromise = TestUtils.topicObserved("mocked-protocol-handler"); + + // Load the URL: + const kURL = "local-app-test:redirect"; + let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService( + Ci.nsICommandLineHandler + ); + let fakeCmdLine = makeCmdLineHelper( + TEST_PATH + "redirect_helper.sjs?uri=" + encodeURIComponent(kURL) + ); + cmdLineHandler.handle(fakeCmdLine); + + // Check that the mock app was launched. If the dialog showed instead, + // the test will fail. + let [uri] = await loadPromise; + is(uri.spec, "local-app-test:redirect", "Should have seen correct URI."); + // We might have opened a blank tab, see bug 1718104 and friends. + if (gBrowser.selectedTab != initialTab) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + + // We do this both here and in cleanup so it's easy to add tasks to this test, + // and so we clean up correctly if the test aborts before we get here. + document.documentElement.removeEventListener( + "dialogopen", + dialogOpenListener + ); + gHandlerService.wrappedJSObject.mockProtocolHandler(); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js new file mode 100644 index 0000000000..7d51a9c59a --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js @@ -0,0 +1,1348 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const { ExtensionPermissions } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); + +let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +const ROOT_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "" +); + +// Testing multiple protocol / origin combinations takes long on debug. +requestLongerTimeout(7); + +const DIALOG_URL_APP_CHOOSER = + "chrome://mozapps/content/handling/appChooser.xhtml"; +const DIALOG_URL_PERMISSION = + "chrome://mozapps/content/handling/permissionDialog.xhtml"; + +const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler"; +const PERMISSION_KEY_DELIMITER = "^"; + +const TEST_PROTOS = ["foo", "bar"]; + +let testDir = getChromeDir(getResolvedURI(gTestPath)); + +const ORIGIN1 = "https://example.com"; +const ORIGIN2 = "https://example.org"; +const ORIGIN3 = Services.io.newFileURI(testDir).spec; +const PRINCIPAL1 = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN1); +const PRINCIPAL2 = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN2); +const PRINCIPAL3 = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN3); + +const NULL_PRINCIPAL_SCHEME = Services.scriptSecurityManager + .createNullPrincipal({}) + .scheme.toLowerCase(); + +/** + * Get the open protocol handler permission key for a given protocol scheme. + * @param {string} aProtocolScheme - Scheme of protocol to construct permission + * key with. + */ +function getSkipProtoDialogPermissionKey(aProtocolScheme) { + return ( + PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER + aProtocolScheme + ); +} + +/** + * Creates dummy web protocol handlers used for testing. + */ +function initTestHandlers() { + TEST_PROTOS.forEach(scheme => { + let webHandler = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + webHandler.name = scheme + "Handler"; + webHandler.uriTemplate = ORIGIN1 + "/?url=%s"; + + let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(scheme); + handlerInfo.possibleApplicationHandlers.appendElement(webHandler); + handlerInfo.preferredApplicationHandler = webHandler; + gHandlerService.store(handlerInfo); + }); +} + +/** + * Update whether the protocol handler dialog is shown for our test protocol + + * handler. + * @param {string} scheme - Scheme of the protocol to change the ask state for. + * @param {boolean} ask - true => show dialog, false => skip dialog. + */ +function updateAlwaysAsk(scheme, ask) { + let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(scheme); + handlerInfo.alwaysAskBeforeHandling = ask; + gHandlerService.store(handlerInfo); +} + +/** + * Test whether the protocol handler dialog is set to show for our + * test protocol + handler. + * @param {string} scheme - Scheme of the protocol to test the ask state for. + * @param {boolean} ask - true => show dialog, false => skip dialog. + */ +function testAlwaysAsk(scheme, ask) { + is( + HandlerServiceTestUtils.getHandlerInfo(scheme).alwaysAskBeforeHandling, + ask, + "Should have correct alwaysAsk state." + ); +} + +/** + * Triggers the load via a server redirect. + * @param {string} serverRedirect - The redirect type. + */ +function useServerRedirect(serverRedirect) { + return async (browser, scheme) => { + let uri = `${scheme}://test`; + + let innerParams = new URLSearchParams(); + innerParams.set("uri", uri); + innerParams.set("redirectType", serverRedirect); + let params = new URLSearchParams(); + params.set( + "uri", + "https://example.com/" + + ROOT_PATH + + "redirect_helper.sjs?" + + innerParams.toString() + ); + uri = + "https://example.org/" + + ROOT_PATH + + "redirect_helper.sjs?" + + params.toString(); + BrowserTestUtils.loadURIString(browser, uri); + }; +} + +/** + * Triggers the load with a specific principal or the browser's current + * principal. + * @param {nsIPrincipal} [principal] - Principal to use to trigger the load. + */ +function useTriggeringPrincipal(principal = undefined) { + return async (browser, scheme) => { + let uri = `${scheme}://test`; + let triggeringPrincipal = principal ?? browser.contentPrincipal; + + info("Loading uri: " + uri); + browser.loadURI(Services.io.newURI(uri), { triggeringPrincipal }); + }; +} + +/** + * Navigates to a test URL with the given protocol scheme and waits for the + * result. + * @param {MozBrowser} browser - Browser to navigate. + * @param {string} scheme - Scheme of the test url. e.g. irc + * @param {Object} [options] - Test options. + * @param {Object} [options.permDialogOptions] - Test options for the permission + * dialog. If defined, we expect this dialog to be shown. + * @param {Object} [options.chooserDialogOptions] - Test options for the chooser + * dialog. If defined, we expect this dialog to be shown. + * @param {Function} [options.triggerLoad] - An async callback function to + * trigger the load. Will be passed the browser and scheme to use. + * @param {nsIPrincipal} [options.triggeringPrincipal] - Principal to trigger + * the load with. Defaults to the browsers content principal. + * @returns {Promise} - A promise which resolves once the test is complete. + */ +async function testOpenProto( + browser, + scheme, + { + permDialogOptions, + chooserDialogOptions, + triggerLoad = useTriggeringPrincipal(), + } = {} +) { + let permDialogOpenPromise; + let chooserDialogOpenPromise; + + if (permDialogOptions) { + info("Should see permission dialog"); + permDialogOpenPromise = waitForProtocolPermissionDialog(browser, true); + } + + if (chooserDialogOptions) { + info("Should see chooser dialog"); + chooserDialogOpenPromise = waitForProtocolAppChooserDialog(browser, true); + } + await triggerLoad(browser, scheme); + let webHandlerLoadedPromise; + + let webHandlerShouldOpen = + (!permDialogOptions && !chooserDialogOptions) || + ((permDialogOptions?.actionConfirm || permDialogOptions?.actionChangeApp) && + chooserDialogOptions?.actionConfirm); + + // Register web handler load listener if we expect to trigger it. + if (webHandlerShouldOpen) { + webHandlerLoadedPromise = waitForHandlerURL(browser, scheme); + } + + if (permDialogOpenPromise) { + let dialog = await permDialogOpenPromise; + let dialogEl = getDialogElementFromSubDialog(dialog); + let dialogType = getDialogType(dialog); + + let { + hasCheckbox, + checkboxOrigin, + hasChangeApp, + chooserIsNext, + actionCheckbox, + actionConfirm, + actionChangeApp, + checkContents, + } = permDialogOptions; + + if (actionChangeApp) { + actionConfirm = false; + } + + let descriptionEl = dialogEl.querySelector("#description"); + ok( + descriptionEl && BrowserTestUtils.is_visible(descriptionEl), + "Has a visible description element." + ); + + ok( + !descriptionEl.innerHTML.toLowerCase().includes(NULL_PRINCIPAL_SCHEME), + "Description does not include NullPrincipal scheme." + ); + + await testCheckbox(dialogEl, dialogType, { + hasCheckbox, + actionCheckbox, + checkboxOrigin, + }); + + // Check the button label depending on whether we would show the chooser + // dialog next or directly open the handler. + let acceptBtnLabel = dialogEl.getButton("accept")?.label; + + if (chooserIsNext) { + is( + acceptBtnLabel, + "Choose Application", + "Accept button has choose app label" + ); + } else { + is(acceptBtnLabel, "Open Link", "Accept button has open link label"); + } + + let changeAppLink = dialogEl.ownerDocument.getElementById("change-app"); + if (typeof hasChangeApp == "boolean") { + ok(changeAppLink, "Permission dialog should have changeApp link label"); + is( + !changeAppLink.hidden, + hasChangeApp, + "Permission dialog change app link label" + ); + } + + if (checkContents) { + checkContents(dialogEl); + } + + if (actionChangeApp) { + let dialogClosedPromise = waitForProtocolPermissionDialog(browser, false); + changeAppLink.click(); + await dialogClosedPromise; + } else { + await closeDialog(browser, dialog, actionConfirm, scheme); + } + } + + if (chooserDialogOpenPromise) { + let dialog = await chooserDialogOpenPromise; + let dialogEl = getDialogElementFromSubDialog(dialog); + let dialogType = getDialogType(dialog); + + let { hasCheckbox, actionCheckbox, actionConfirm } = chooserDialogOptions; + + await testCheckbox(dialogEl, dialogType, { + hasCheckbox, + actionCheckbox, + }); + + await closeDialog(browser, dialog, actionConfirm, scheme); + } + + if (webHandlerShouldOpen) { + info("Waiting for web handler to open"); + await webHandlerLoadedPromise; + } else { + info("Web handler open canceled"); + } +} + +/** + * Inspects the checkbox state and interacts with it. + * @param {dialog} dialogEl + * @param {string} dialogType - String identifier of dialog type. + * Either "permission" or "chooser". + * @param {Object} options - Test Options. + * @param {boolean} [options.hasCheckbox] - Whether the dialog is expected to + * have a visible checkbox. + * @param {boolean} [options.hasCheckboxState] - The check state of the checkbox + * to test for. true = checked, false = unchecked. + * @param {boolean} [options.actionCheckbox] - The state to set on the checkbox. + * true = checked, false = unchecked. + */ +async function testCheckbox( + dialogEl, + dialogType, + { hasCheckbox, hasCheckboxState = false, actionCheckbox, checkboxOrigin } +) { + let checkbox = dialogEl.ownerDocument.getElementById("remember"); + if (typeof hasCheckbox == "boolean") { + is( + checkbox && BrowserTestUtils.is_visible(checkbox), + hasCheckbox, + "Dialog checkbox has correct visibility." + ); + + let checkboxLabel = dialogEl.ownerDocument.getElementById("remember-label"); + is( + checkbox && BrowserTestUtils.is_visible(checkboxLabel), + hasCheckbox, + "Dialog checkbox label has correct visibility." + ); + if (hasCheckbox) { + ok( + !checkboxLabel.innerHTML.toLowerCase().includes(NULL_PRINCIPAL_SCHEME), + "Dialog checkbox label does not include NullPrincipal scheme." + ); + } + } + + if (typeof hasCheckboxState == "boolean") { + is(checkbox.checked, hasCheckboxState, "Dialog checkbox has correct state"); + } + + if (checkboxOrigin) { + let doc = dialogEl.ownerDocument; + let hostFromLabel = doc.l10n.getAttributes( + doc.getElementById("remember-label") + ).args.host; + is(hostFromLabel, checkboxOrigin, "Checkbox should be for correct domain."); + } + + if (typeof actionCheckbox == "boolean") { + checkbox.click(); + } +} + +/** + * Get the dialog element which is a child of the SubDialogs browser frame. + * @param {SubDialog} subDialog - Dialog to get the dialog element for. + */ +function getDialogElementFromSubDialog(subDialog) { + let dialogEl = subDialog._frame.contentDocument.querySelector("dialog"); + ok(dialogEl, "SubDialog should have dialog element"); + return dialogEl; +} + +/** + * Wait for the test handler to be opened. + * @param {MozBrowser} browser - The browser the load should occur in. + * @param {string} scheme - Scheme which triggered the handler to open. + */ +function waitForHandlerURL(browser, scheme) { + return BrowserTestUtils.browserLoaded( + browser, + false, + url => url == `${ORIGIN1}/?url=${scheme}%3A%2F%2Ftest` + ); +} + +/** + * Test for open-protocol-handler permission. + * @param {nsIPrincipal} principal - The principal to test the permission on. + * @param {string} scheme - Scheme to generate permission key. + * @param {boolean} hasPerm - Whether we expect the princial to set the + * permission (true), or not (false). + */ +function testPermission(principal, scheme, hasPerm) { + let permKey = getSkipProtoDialogPermissionKey(scheme); + let result = Services.perms.testPermissionFromPrincipal(principal, permKey); + let message = `${permKey} ${hasPerm ? "is" : "is not"} set for ${ + principal.origin + }.`; + is(result == Services.perms.ALLOW_ACTION, hasPerm, message); +} + +/** + * Get the checkbox element of the dialog used to remember the handler choice or + * store the permission. + * @param {SubDialog} dialog - Protocol handler dialog embedded in a SubDialog. + * @param {string} dialogType - Type of the dialog which holds the checkbox. + * @returns {HTMLInputElement} - Checkbox of the dialog. + */ +function getDialogCheckbox(dialog, dialogType) { + let id; + if (dialogType == "permission") { + id = "remember-permission"; + } else { + id = "remember"; + } + return dialog._frame.contentDocument.getElementById(id); +} + +function getDialogType(dialog) { + let url = dialog._frame.currentURI.spec; + + if (url === DIALOG_URL_PERMISSION) { + return "permission"; + } + if (url === DIALOG_URL_APP_CHOOSER) { + return "chooser"; + } + throw new Error("Dialog with unexpected url"); +} + +/** + * Exit a protocol handler SubDialog and wait for it to be fully closed. + * @param {MozBrowser} browser - Browser element of the tab where the dialog is + * shown. + * @param {SubDialog} dialog - SubDialog object which holds the protocol handler + * @param {boolean} confirm - Whether to confirm (true) or cancel (false) the + * dialog. + * @param {string} scheme - The scheme of the protocol the dialog is opened for. + * dialog. + */ +async function closeDialog(browser, dialog, confirm, scheme) { + let dialogClosedPromise = waitForSubDialog(browser, dialog._openedURL, false); + let dialogEl = getDialogElementFromSubDialog(dialog); + + if (confirm) { + if (getDialogType(dialog) == "chooser") { + // Select our test protocol handler + let listItem = dialogEl.ownerDocument.querySelector( + `richlistitem[name="${scheme}Handler"]` + ); + listItem.click(); + } + + dialogEl.setAttribute("buttondisabledaccept", false); + dialogEl.acceptDialog(); + } else { + dialogEl.cancelDialog(); + } + + return dialogClosedPromise; +} + +registerCleanupFunction(function () { + // Clean up test handlers + TEST_PROTOS.forEach(scheme => { + let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(scheme); + gHandlerService.remove(handlerInfo); + }); + + // Clear permissions + Services.perms.removeAll(); +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["security.external_protocol_requires_permission", true]], + }); + initTestHandlers(); +}); + +/** + * Tests that when "remember" is unchecked, we only allow the protocol to be + * opened once and don't store any permission. + */ +add_task(async function test_permission_allow_once() { + for (let scheme of TEST_PROTOS) { + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionConfirm: true, + }, + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + // No permission should be set + testPermission(PRINCIPAL1, scheme, false); + testPermission(PRINCIPAL2, scheme, false); + + // No preferred app should be set + testAlwaysAsk(scheme, true); + + // If we open again we should see the permission dialog + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionConfirm: false, + }, + }); + }); + } +}); + +/** + * Tests that when checking the "remember" checkbox, the protocol permission + * is set correctly and allows the caller to skip the permission dialog in + * subsequent calls. + */ +add_task(async function test_permission_allow_persist() { + for (let [origin, principal] of [ + [ORIGIN1, PRINCIPAL1], + [ORIGIN3, PRINCIPAL3], + ]) { + for (let scheme of TEST_PROTOS) { + info("Testing with origin " + origin); + info("testing with principal of origin " + principal.origin); + info("testing with protocol " + scheme); + + // Set a permission for an unrelated protocol. + // We should still see the permission dialog. + Services.perms.addFromPrincipal( + principal, + getSkipProtoDialogPermissionKey("foobar"), + Services.perms.ALLOW_ACTION + ); + + await BrowserTestUtils.withNewTab(origin, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionCheckbox: true, + actionConfirm: true, + }, + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + // Permission should be set + testPermission(principal, scheme, true); + testPermission(PRINCIPAL2, scheme, false); + + // No preferred app should be set + testAlwaysAsk(scheme, true); + + // If we open again with the origin where we granted permission, we should + // directly get the chooser dialog. + await BrowserTestUtils.withNewTab(origin, async browser => { + await testOpenProto(browser, scheme, { + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, + }, + }); + }); + + // If we open with the other origin, we should see the permission dialog + await BrowserTestUtils.withNewTab(ORIGIN2, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionConfirm: false, + }, + }); + }); + + // Cleanup permissions + Services.perms.removeAll(); + } + } +}); + +/** + * Tests that if a preferred protocol handler is set, the permission dialog + * shows the application name and a link which leads to the app chooser. + */ +add_task(async function test_permission_application_set() { + let scheme = TEST_PROTOS[0]; + updateAlwaysAsk(scheme, false); + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: true, + chooserIsNext: false, + actionChangeApp: true, + }, + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + // Cleanup + updateAlwaysAsk(scheme, true); +}); + +/** + * Tests that we correctly handle system principals. They should always + * skip the permission dialog. + */ +add_task(async function test_permission_system_principal() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + chooserDialogOptions: { hasCheckbox: true, actionConfirm: false }, + triggerLoad: useTriggeringPrincipal( + Services.scriptSecurityManager.getSystemPrincipal() + ), + }); + }); +}); + +/** + * Tests that we don't show the permission dialog if the permission is disabled + * by pref. + */ +add_task(async function test_permission_disabled() { + let scheme = TEST_PROTOS[0]; + + await SpecialPowers.pushPrefEnv({ + set: [["security.external_protocol_requires_permission", false]], + }); + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + await SpecialPowers.popPrefEnv(); +}); + +/** + * Tests that we directly open the handler if permission and handler are set. + */ +add_task(async function test_app_and_permission_set() { + let scheme = TEST_PROTOS[1]; + + updateAlwaysAsk(scheme, false); + Services.perms.addFromPrincipal( + PRINCIPAL2, + getSkipProtoDialogPermissionKey(scheme), + Services.perms.ALLOW_ACTION + ); + + await BrowserTestUtils.withNewTab(ORIGIN2, async browser => { + await testOpenProto(browser, scheme); + }); + + // Cleanup + Services.perms.removeAll(); + updateAlwaysAsk(scheme, true); +}); + +/** + * Tests that the alwaysAsk state is not updated if the user cancels the dialog + */ +add_task(async function test_change_app_checkbox_cancel() { + let scheme = TEST_PROTOS[0]; + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + chooserIsNext: true, + hasChangeApp: false, + actionConfirm: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionCheckbox: true, // Activate checkbox + actionConfirm: false, // Cancel dialog + }, + }); + }); + + // Should not have applied value from checkbox + testAlwaysAsk(scheme, true); +}); + +/** + * Tests that the external protocol dialogs behave correctly when a null + * principal is passed. + */ +add_task(async function test_null_principal() { + let scheme = TEST_PROTOS[0]; + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + triggerLoad: () => { + let uri = `${scheme}://test`; + ContentTask.spawn(browser, { uri }, args => { + let frame = content.document.createElement("iframe"); + frame.src = `data:text/html,`; + content.document.body.appendChild(frame); + }); + }, + permDialogOptions: { + hasCheckbox: false, + chooserIsNext: true, + hasChangeApp: false, + actionConfirm: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); + +/** + * Tests that the external protocol dialogs behave correctly when no principal + * is passed. + */ +add_task(async function test_no_principal() { + let scheme = TEST_PROTOS[1]; + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + triggerLoad: () => { + let uri = `${scheme}://test`; + + let contentDispatchChooser = Cc[ + "@mozilla.org/content-dispatch-chooser;1" + ].createInstance(Ci.nsIContentDispatchChooser); + + let handler = HandlerServiceTestUtils.getHandlerInfo(scheme); + + contentDispatchChooser.handleURI( + handler, + Services.io.newURI(uri), + null, + browser.browsingContext + ); + }, + permDialogOptions: { + hasCheckbox: false, + chooserIsNext: true, + hasChangeApp: false, + actionConfirm: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); + +/** + * Tests that if a URI scheme has a non-standard protocol, an OS default exists, + * and the user hasn't selected an alternative only the permission dialog is shown. + */ +add_task(async function test_non_standard_protocol() { + let scheme = null; + // TODO add a scheme for Windows 10 or greater once support is added (see bug 1764599). + if (AppConstants.platform == "macosx") { + scheme = "itunes"; + } else { + info( + "Skipping this test since there isn't a suitable default protocol on this platform" + ); + return; + } + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: true, + chooserIsNext: false, + actionChangeApp: false, + }, + }); + }); +}); + +/** + * Tests that we show the permission dialog for extension content scripts. + */ +add_task(async function test_extension_content_script_permission() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + let testExtension; + + await testOpenProto(browser, scheme, { + triggerLoad: async () => { + let uri = `${scheme}://test`; + + const EXTENSION_DATA = { + manifest: { + content_scripts: [ + { + matches: [browser.currentURI.spec], + js: ["navigate.js"], + }, + ], + browser_specific_settings: { + gecko: { id: "allowed@mochi.test" }, + }, + }, + files: { + "navigate.js": `window.location.href = "${uri}";`, + }, + useAddonManager: "permanent", + }; + + testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA); + await testExtension.startup(); + }, + permDialogOptions: { + hasCheckbox: true, + chooserIsNext: true, + hasChangeApp: false, + actionCheckbox: true, + actionConfirm: true, + checkContents: dialogEl => { + let description = dialogEl.querySelector("#description"); + let { id, args } = + description.ownerDocument.l10n.getAttributes(description); + is( + id, + "permission-dialog-description-extension", + "Should be using the correct string." + ); + is( + args.extension, + "Generated extension", + "Should have the correct extension name." + ); + }, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + + let extensionPrincipal = + Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(`moz-extension://${testExtension.uuid}/`), + {} + ); + let extensionPrivatePrincipal = + Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(`moz-extension://${testExtension.uuid}/`), + { privateBrowsingId: 1 } + ); + + let key = getSkipProtoDialogPermissionKey(scheme); + is( + Services.perms.testPermissionFromPrincipal(extensionPrincipal, key), + Services.perms.ALLOW_ACTION, + "Should have permanently allowed the extension" + ); + is( + Services.perms.testPermissionFromPrincipal( + extensionPrivatePrincipal, + key + ), + Services.perms.UNKNOWN_ACTION, + "Should not have changed the private principal permission" + ); + is( + Services.perms.testPermissionFromPrincipal(PRINCIPAL1, key), + Services.perms.UNKNOWN_ACTION, + "Should not have allowed the page" + ); + + await testExtension.unload(); + + is( + Services.perms.testPermissionFromPrincipal(extensionPrincipal, key), + Services.perms.UNKNOWN_ACTION, + "Should have cleared the extension's normal principal permission" + ); + is( + Services.perms.testPermissionFromPrincipal( + extensionPrivatePrincipal, + key + ), + Services.perms.UNKNOWN_ACTION, + "Should have cleared the private browsing principal" + ); + }); +}); + +/** + * Tests that we show the permission dialog for extension content scripts. + */ +add_task(async function test_extension_private_content_script_permission() { + let scheme = TEST_PROTOS[0]; + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: ORIGIN1 }, + async browser => { + let testExtension; + + await testOpenProto(browser, scheme, { + triggerLoad: async () => { + let uri = `${scheme}://test`; + + const EXTENSION_DATA = { + manifest: { + content_scripts: [ + { + matches: [browser.currentURI.spec], + js: ["navigate.js"], + }, + ], + browser_specific_settings: { + gecko: { id: "allowed@mochi.test" }, + }, + }, + files: { + "navigate.js": `window.location.href = "${uri}";`, + }, + useAddonManager: "permanent", + }; + + testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA); + await testExtension.startup(); + let perms = { + permissions: ["internal:privateBrowsingAllowed"], + origins: [], + }; + await ExtensionPermissions.add("allowed@mochi.test", perms); + let addon = await AddonManager.getAddonByID("allowed@mochi.test"); + await addon.reload(); + }, + permDialogOptions: { + hasCheckbox: true, + chooserIsNext: true, + hasChangeApp: false, + actionCheckbox: true, + actionConfirm: true, + checkContents: dialogEl => { + let description = dialogEl.querySelector("#description"); + let { id, args } = + description.ownerDocument.l10n.getAttributes(description); + is( + id, + "permission-dialog-description-extension", + "Should be using the correct string." + ); + is( + args.extension, + "Generated extension", + "Should have the correct extension name." + ); + }, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + + let extensionPrincipal = + Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(`moz-extension://${testExtension.uuid}/`), + {} + ); + let extensionPrivatePrincipal = + Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(`moz-extension://${testExtension.uuid}/`), + { privateBrowsingId: 1 } + ); + + let key = getSkipProtoDialogPermissionKey(scheme); + is( + Services.perms.testPermissionFromPrincipal(extensionPrincipal, key), + Services.perms.UNKNOWN_ACTION, + "Should not have changed the extension's normal principal permission" + ); + is( + Services.perms.testPermissionFromPrincipal( + extensionPrivatePrincipal, + key + ), + Services.perms.ALLOW_ACTION, + "Should have allowed the private browsing principal" + ); + is( + Services.perms.testPermissionFromPrincipal(PRINCIPAL1, key), + Services.perms.UNKNOWN_ACTION, + "Should not have allowed the page" + ); + + await testExtension.unload(); + + is( + Services.perms.testPermissionFromPrincipal(extensionPrincipal, key), + Services.perms.UNKNOWN_ACTION, + "Should have cleared the extension's normal principal permission" + ); + is( + Services.perms.testPermissionFromPrincipal( + extensionPrivatePrincipal, + key + ), + Services.perms.UNKNOWN_ACTION, + "Should have cleared the private browsing principal" + ); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests that we do not show the permission dialog for extension content scripts + * when the page already has permission. + */ +add_task(async function test_extension_allowed_content() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + let testExtension; + + let key = getSkipProtoDialogPermissionKey(scheme); + Services.perms.addFromPrincipal( + PRINCIPAL1, + key, + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_NEVER + ); + + await testOpenProto(browser, scheme, { + triggerLoad: async () => { + let uri = `${scheme}://test`; + + const EXTENSION_DATA = { + manifest: { + content_scripts: [ + { + matches: [browser.currentURI.spec], + js: ["navigate.js"], + }, + ], + }, + files: { + "navigate.js": `window.location.href = "${uri}";`, + }, + }; + + testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA); + await testExtension.startup(); + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + + let extensionPrincipal = + Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(`moz-extension://${testExtension.uuid}/`), + {} + ); + + is( + Services.perms.testPermissionFromPrincipal(extensionPrincipal, key), + Services.perms.UNKNOWN_ACTION, + "Should not have permanently allowed the extension" + ); + + await testExtension.unload(); + Services.perms.removeFromPrincipal(PRINCIPAL1, key); + }); +}); + +/** + * Tests that we do not show the permission dialog for extension content scripts + * when the extension already has permission. + */ +add_task(async function test_extension_allowed_extension() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + let testExtension; + + let key = getSkipProtoDialogPermissionKey(scheme); + + await testOpenProto(browser, scheme, { + triggerLoad: async () => { + const EXTENSION_DATA = { + manifest: { + permissions: [`${ORIGIN1}/*`], + }, + background() { + browser.test.onMessage.addListener(async (msg, uri) => { + switch (msg) { + case "engage": + browser.tabs.executeScript({ + code: `window.location.href = "${uri}";`, + }); + break; + default: + browser.test.fail(`Unexpected message received: ${msg}`); + } + }); + }, + }; + + testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA); + await testExtension.startup(); + + let extensionPrincipal = + Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(`moz-extension://${testExtension.uuid}/`), + {} + ); + Services.perms.addFromPrincipal( + extensionPrincipal, + key, + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_NEVER + ); + + testExtension.sendMessage("engage", `${scheme}://test`); + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + + await testExtension.unload(); + Services.perms.removeFromPrincipal(PRINCIPAL1, key); + }); +}); + +/** + * Tests that we show the permission dialog for extensions directly opening a + * protocol. + */ +add_task(async function test_extension_principal() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + let testExtension; + + await testOpenProto(browser, scheme, { + triggerLoad: async () => { + const EXTENSION_DATA = { + background() { + browser.test.onMessage.addListener(async (msg, url) => { + switch (msg) { + case "engage": + browser.tabs.update({ + url, + }); + break; + default: + browser.test.fail(`Unexpected message received: ${msg}`); + } + }); + }, + }; + + testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA); + await testExtension.startup(); + testExtension.sendMessage("engage", `${scheme}://test`); + }, + permDialogOptions: { + hasCheckbox: true, + chooserIsNext: true, + hasChangeApp: false, + actionCheckbox: true, + actionConfirm: true, + checkContents: dialogEl => { + let description = dialogEl.querySelector("#description"); + let { id, args } = + description.ownerDocument.l10n.getAttributes(description); + is( + id, + "permission-dialog-description-extension", + "Should be using the correct string." + ); + is( + args.extension, + "Generated extension", + "Should have the correct extension name." + ); + }, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + + let extensionPrincipal = + Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(`moz-extension://${testExtension.uuid}/`), + {} + ); + + let key = getSkipProtoDialogPermissionKey(scheme); + is( + Services.perms.testPermissionFromPrincipal(extensionPrincipal, key), + Services.perms.ALLOW_ACTION, + "Should have permanently allowed the extension" + ); + is( + Services.perms.testPermissionFromPrincipal(PRINCIPAL1, key), + Services.perms.UNKNOWN_ACTION, + "Should not have allowed the page" + ); + + await testExtension.unload(); + }); +}); + +/** + * Test that we use the redirect principal for the dialog when applicable. + */ +add_task(async function test_redirect_principal() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab("about:blank", async browser => { + await testOpenProto(browser, scheme, { + triggerLoad: useServerRedirect("location"), + permDialogOptions: { + checkboxOrigin: ORIGIN1, + chooserIsNext: true, + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); + +/** + * Test that we use the redirect principal for the dialog for refresh headers. + */ +add_task(async function test_redirect_principal() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab("about:blank", async browser => { + await testOpenProto(browser, scheme, { + triggerLoad: useServerRedirect("refresh"), + permDialogOptions: { + checkboxOrigin: ORIGIN1, + chooserIsNext: true, + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); + +/** + * Test that we use the redirect principal for the dialog for meta refreshes. + */ +add_task(async function test_redirect_principal() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab("about:blank", async browser => { + await testOpenProto(browser, scheme, { + triggerLoad: useServerRedirect("meta-refresh"), + permDialogOptions: { + checkboxOrigin: ORIGIN1, + chooserIsNext: true, + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); + +/** + * Test that we use the redirect principal for the dialog for JS redirects. + */ +add_task(async function test_redirect_principal_js() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab("about:blank", async browser => { + await testOpenProto(browser, scheme, { + triggerLoad: () => { + let uri = `${scheme}://test`; + + let innerParams = new URLSearchParams(); + innerParams.set("uri", uri); + let params = new URLSearchParams(); + params.set( + "uri", + "https://example.com/" + + ROOT_PATH + + "script_redirect.html?" + + innerParams.toString() + ); + uri = + "https://example.org/" + + ROOT_PATH + + "script_redirect.html?" + + params.toString(); + BrowserTestUtils.loadURIString(browser, uri); + }, + permDialogOptions: { + checkboxOrigin: ORIGIN1, + chooserIsNext: true, + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); + +/** + * Test that we use the redirect principal for the dialog for link clicks. + */ +add_task(async function test_redirect_principal_links() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab("about:blank", async browser => { + await testOpenProto(browser, scheme, { + triggerLoad: async () => { + let uri = `${scheme}://test`; + + let params = new URLSearchParams(); + params.set("uri", uri); + uri = + "https://example.com/" + + ROOT_PATH + + "redirect_helper.sjs?" + + params.toString(); + await ContentTask.spawn(browser, { uri }, args => { + let textLink = content.document.createElement("a"); + textLink.href = args.uri; + textLink.textContent = "click me"; + content.document.body.appendChild(textLink); + textLink.click(); + }); + }, + permDialogOptions: { + checkboxOrigin: ORIGIN1, + chooserIsNext: true, + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox.js b/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox.js new file mode 100644 index 0000000000..bc4b13730a --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox.js @@ -0,0 +1,140 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests navigation to external protocol from sandboxed iframes. + */ + +"use strict"; + +requestLongerTimeout(2); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.block_external_protocol_navigation_from_sandbox", true]], + }); + + await setupMailHandler(); +}); + +add_task(async function test_sandbox_disabled() { + await runExtProtocolSandboxTest({ blocked: false, sandbox: null }); +}); + +add_task(async function test_sandbox_allowed() { + let flags = [ + "allow-popups", + "allow-top-navigation", + "allow-top-navigation-by-user-activation", + "allow-top-navigation-to-custom-protocols", + ]; + + for (let flag of flags) { + await runExtProtocolSandboxTest({ + blocked: false, + sandbox: `allow-scripts ${flag}`, + }); + } +}); + +add_task(async function test_sandbox_blocked() { + let flags = [ + "", + "allow-same-origin", + "allow-forms", + "allow-scripts", + "allow-pointer-lock", + "allow-orientation-lock", + "allow-modals", + "allow-popups-to-escape-sandbox", + "allow-presentation", + "allow-storage-access-by-user-activation", + "allow-downloads", + ]; + + for (let flag of flags) { + await runExtProtocolSandboxTest({ + blocked: true, + sandbox: `allow-scripts ${flag}`, + }); + } +}); + +add_task(async function test_sandbox_blocked_triggers() { + info( + "For sandboxed frames external protocol navigation is blocked, no matter how it is triggered." + ); + for (let triggerMethod of [ + "trustedClick", + "untrustedClick", + "trustedLocationAPI", + "untrustedLocationAPI", + "frameSrc", + ]) { + await runExtProtocolSandboxTest({ + blocked: true, + sandbox: "allow-scripts", + triggerMethod, + }); + } + + info( + "When allow-top-navigation-by-user-activation navigation to external protocols with transient user activations is allowed." + ); + await runExtProtocolSandboxTest({ + blocked: false, + sandbox: "allow-scripts allow-top-navigation-by-user-activation", + triggerMethod: "trustedClick", + }); + + await runExtProtocolSandboxTest({ + blocked: true, + sandbox: "allow-scripts allow-top-navigation-by-user-activation", + triggerMethod: "untrustedClick", + }); + + await runExtProtocolSandboxTest({ + blocked: true, + sandbox: "allow-scripts allow-top-navigation-by-user-activation", + triggerMethod: "untrustedLocationAPI", + }); + + await runExtProtocolSandboxTest({ + blocked: true, + sandbox: "allow-scripts allow-top-navigation-by-user-activation", + triggerMethod: "frameSrc", + }); +}); + +add_task(async function test_sandbox_combination() { + await runExtProtocolSandboxTest({ + blocked: false, + sandbox: + "allow-scripts allow-downloads allow-top-navigation-to-custom-protocols", + }); + + await runExtProtocolSandboxTest({ + blocked: false, + sandbox: + "allow-scripts allow-top-navigation allow-top-navigation-to-custom-protocols", + }); + + await runExtProtocolSandboxTest({ + blocked: true, + sandbox: "allow-scripts allow-modals", + }); +}); + +add_task(async function test_sandbox_iframe_redirect() { + await runExtProtocolSandboxTest({ + blocked: true, + sandbox: "allow-scripts", + triggerMethod: "frameSrcRedirect", + }); + + await runExtProtocolSandboxTest({ + blocked: false, + sandbox: "allow-scripts allow-top-navigation-to-custom-protocols", + triggerMethod: "frameSrcRedirect", + }); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox_csp.js b/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox_csp.js new file mode 100644 index 0000000000..03d2fe8cf5 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox_csp.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests navigation to external protocol from csp-sandboxed iframes. + */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.block_external_protocol_navigation_from_sandbox", true]], + }); + + await setupMailHandler(); +}); + +add_task(async function test_sandbox_csp() { + for (let triggerMethod of [ + "trustedClick", + "untrustedClick", + "trustedLocationAPI", + "untrustedLocationAPI", + ]) { + await runExtProtocolSandboxTest({ + blocked: false, + sandbox: "allow-scripts", + useCSPSandbox: true, + triggerMethod, + }); + } + + await runExtProtocolSandboxTest({ + blocked: false, + sandbox: "allow-scripts allow-top-navigation-to-custom-protocols", + useCSPSandbox: true, + }); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js new file mode 100644 index 0000000000..149701fb23 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_helperapp() { + // Set up the test infrastructure: + const kProt = "foopydoopydoo"; + const extProtocolSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + let handlerInfo = extProtocolSvc.getProtocolHandlerInfo(kProt); + if (handlerSvc.exists(handlerInfo)) { + handlerSvc.fillHandlerInfo(handlerInfo, ""); + } + // Say we want to use a specific app: + handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + handlerInfo.alwaysAskBeforeHandling = false; + + // Say it's us: + let selfFile = Services.dirsvc.get("XREExeF", Ci.nsIFile); + // Make sure it's the .app + if (AppConstants.platform == "macosx") { + while ( + !selfFile.leafName.endsWith(".app") && + !selfFile.leafName.endsWith(".app/") + ) { + selfFile = selfFile.parent; + } + } + let selfHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + selfHandlerApp.executable = selfFile; + handlerInfo.possibleApplicationHandlers.appendElement(selfHandlerApp); + handlerInfo.preferredApplicationHandler = selfHandlerApp; + handlerSvc.store(handlerInfo); + + await BrowserTestUtils.withNewTab("about:blank", async browser => { + // Now, do some safety stubbing. If we do end up recursing we spawn + // infinite tabs. We definitely don't want that. Avoid it by stubbing + // our external URL handling bits: + let oldAddTab = gBrowser.addTab; + registerCleanupFunction(() => (gBrowser.addTab = oldAddTab)); + let wrongThingHappenedPromise = new Promise(resolve => { + gBrowser.addTab = function (aURI) { + ok(false, "Tried to open unexpected URL in a tab: " + aURI); + resolve(null); + // Pass a dummy object to avoid upsetting BrowserContentHandler - + // if it thinks opening the tab failed, it tries to open a window instead, + // which we can't prevent as easily, and at which point we still end up + // with runaway tabs. + return {}; + }; + }); + + let askedUserPromise = waitForProtocolAppChooserDialog(browser, true); + + BrowserTestUtils.loadURIString(browser, kProt + ":test"); + let dialog = await Promise.race([ + wrongThingHappenedPromise, + askedUserPromise, + ]); + ok(dialog, "Should have gotten a dialog"); + + let closePromise = waitForProtocolAppChooserDialog(browser, false); + dialog.close(); + await closePromise; + askedUserPromise = null; + }); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js new file mode 100644 index 0000000000..f3fedab69c --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js @@ -0,0 +1,61 @@ +add_task(async function () { + // create mocked objects + let launcher = createMockedObjects(true); + + // open helper app dialog with mocked launcher + let dlg = await openHelperAppDialog(launcher); + + let doc = dlg.document; + let dialogElement = doc.getElementById("unknownContentType"); + + // Set remember choice + ok( + !doc.getElementById("rememberChoice").checked, + "Remember choice checkbox should be not checked." + ); + doc.getElementById("rememberChoice").checked = true; + + // Make sure the mock handler information is not in nsIHandlerService + ok( + !gHandlerSvc.exists(launcher.MIMEInfo), + "Should not be in nsIHandlerService." + ); + + // close the dialog by pushing the ok button. + let dialogClosedPromise = BrowserTestUtils.windowClosed(dlg); + // Make sure the ok button is enabled, since the ok button might be disabled by + // EnableDelayHelper mechanism. Please refer the detailed + // https://searchfox.org/mozilla-central/source/toolkit/components/prompts/src/PromptUtils.sys.mjs#51 + dialogElement.getButton("accept").disabled = false; + dialogElement.acceptDialog(); + await dialogClosedPromise; + + // check the mocked handler information is saved in nsIHandlerService + ok(gHandlerSvc.exists(launcher.MIMEInfo), "Should be in nsIHandlerService."); + // check the extension. + var mimeType = gHandlerSvc.getTypeFromExtension("abc"); + is(mimeType, launcher.MIMEInfo.type, "Got correct mime type."); + for (let handlerInfo of gHandlerSvc.enumerate()) { + if (handlerInfo.type == launcher.MIMEInfo.type) { + // check the alwaysAskBeforeHandling + ok( + !handlerInfo.alwaysAskBeforeHandling, + "Should turn off the always ask." + ); + // check the preferredApplicationHandler + ok( + handlerInfo.preferredApplicationHandler.equals( + launcher.MIMEInfo.preferredApplicationHandler + ), + "Should be equal to the mockedHandlerApp." + ); + // check the perferredAction + is( + handlerInfo.preferredAction, + launcher.MIMEInfo.preferredAction, + "Should be equal to Ci.nsIHandlerInfo.useHelperApp." + ); + break; + } + } +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_save_filenames.js b/uriloader/exthandler/tests/mochitest/browser_save_filenames.js new file mode 100644 index 0000000000..f421a7a609 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_save_filenames.js @@ -0,0 +1,823 @@ +// There are at least seven different ways in a which a file can be saved or downloaded. This +// test ensures that the filename is determined correctly when saving in these ways. The seven +// ways are: +// - save the file individually from the File menu +// - save as complete web page (this uses a different codepath than the previous one) +// - dragging an image to the local file system +// - copy an image and paste it as a file to the local file system (windows only) +// - open a link with content-disposition set to attachment +// - open a link with the download attribute +// - save a link or image from the context menu + +requestLongerTimeout(8); + +let types = { + text: "text/plain", + html: "text/html", + png: "image/png", + jpeg: "image/jpeg", + webp: "image/webp", + otherimage: "image/unknown", + // Other js types (application/javascript and text/javascript) are handled by the system + // inconsistently, but application/x-javascript is handled by the external helper app service, + // so it is used here for this test. + js: "application/x-javascript", + binary: "application/octet-stream", + nonsense: "application/x-nonsense", + zip: "application/zip", + json: "application/json", + tar: "application/x-tar", +}; + +const PNG_DATA = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" +); + +const JPEG_DATA = atob( + "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4z" + + "NDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEB" + + "AxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS" + + "0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKz" + + "tLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgEC" + + "BAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpj" + + "ZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6" + + "/9oADAMBAAIRAxEAPwD3+iiigD//2Q==" +); + +const WEBP_DATA = atob( + "UklGRiIAAABXRUJQVlA4TBUAAAAvY8AYAAfQ/4j+B4CE8H+/ENH/VCIA" +); + +const DEFAULT_FILENAME = + AppConstants.platform == "win" ? "Untitled.htm" : "Untitled.html"; + +const PROMISE_FILENAME_TYPE = "application/x-moz-file-promise-dest-filename"; + +let MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +let expectedItems; +let sendAsAttachment = false; +let httpServer = null; + +function handleRequest(aRequest, aResponse) { + const queryString = new URLSearchParams(aRequest.queryString); + let type = queryString.get("type"); + let filename = queryString.get("filename"); + let dispname = queryString.get("dispname"); + + aResponse.setStatusLine(aRequest.httpVersion, 200); + if (type) { + aResponse.setHeader("Content-Type", types[type]); + } + + if (dispname) { + let dispositionType = sendAsAttachment ? "attachment" : "inline"; + aResponse.setHeader( + "Content-Disposition", + dispositionType + ';name="' + dispname + '"' + ); + } else if (filename) { + let dispositionType = sendAsAttachment ? "attachment" : "inline"; + aResponse.setHeader( + "Content-Disposition", + dispositionType + ';filename="' + filename + '"' + ); + } else if (sendAsAttachment) { + aResponse.setHeader("Content-Disposition", "attachment"); + } + + if (type == "png") { + aResponse.write(PNG_DATA); + } else if (type == "jpeg") { + aResponse.write(JPEG_DATA); + } else if (type == "webp") { + aResponse.write(WEBP_DATA); + } else if (type == "html") { + aResponse.write( + "file.invFile" + ); + } else { + aResponse.write("// Some Text"); + } +} + +function handleBasicImageRequest(aRequest, aResponse) { + aResponse.setHeader("Content-Type", "image/png"); + aResponse.write(PNG_DATA); +} + +function handleRedirect(aRequest, aResponse) { + const queryString = new URLSearchParams(aRequest.queryString); + let filename = queryString.get("filename"); + + aResponse.setStatusLine(aRequest.httpVersion, 302); + aResponse.setHeader("Location", "/bell" + filename[0] + "?" + queryString); +} + +function promiseDownloadFinished(list) { + return new Promise(resolve => { + list.addView({ + onDownloadChanged(download) { + if (download.stopped) { + list.removeView(this); + resolve(download); + } + }, + }); + }); +} + +// nsIFile::CreateUnique crops long filenames if the path is too long, but +// we don't know exactly how long depending on the full path length, so +// for those save methods that use CreateUnique, instead just verify that +// the filename starts with the right string and has the correct extension. +function checkShortenedFilename(actual, expected) { + if (actual.length < expected.length) { + let actualDot = actual.lastIndexOf("."); + let actualExtension = actual.substring(actualDot); + let expectedExtension = expected.substring(expected.lastIndexOf(".")); + if ( + actualExtension == expectedExtension && + expected.startsWith(actual.substring(0, actualDot)) + ) { + return true; + } + } + + return false; +} + +add_setup(async function () { + const { HttpServer } = ChromeUtils.import( + "resource://testing-common/httpd.js" + ); + httpServer = new HttpServer(); + httpServer.start(8000); + + // Need to load the page from localhost:8000 as the download attribute + // only applies to links from the same domain. + let saveFilenamesPage = FileUtils.getFile( + "CurWorkD", + "/browser/uriloader/exthandler/tests/mochitest/save_filenames.html".split( + "/" + ) + ); + httpServer.registerFile("/save_filenames.html", saveFilenamesPage); + + // A variety of different scripts are set up to better ensure uniqueness. + httpServer.registerPathHandler("/save_filename.sjs", handleRequest); + httpServer.registerPathHandler("/save_thename.sjs", handleRequest); + httpServer.registerPathHandler("/getdata.png", handleRequest); + httpServer.registerPathHandler("/base", handleRequest); + httpServer.registerPathHandler("/basedata", handleRequest); + httpServer.registerPathHandler("/basetext", handleRequest); + httpServer.registerPathHandler("/text2.txt", handleRequest); + httpServer.registerPathHandler("/text3.gonk", handleRequest); + httpServer.registerPathHandler("/basic.png", handleBasicImageRequest); + httpServer.registerPathHandler("/aquamarine.jpeg", handleBasicImageRequest); + httpServer.registerPathHandler("/lazuli.exe", handleBasicImageRequest); + httpServer.registerPathHandler("/redir", handleRedirect); + httpServer.registerPathHandler("/bellr", handleRequest); + httpServer.registerPathHandler("/bellg", handleRequest); + httpServer.registerPathHandler("/bellb", handleRequest); + httpServer.registerPathHandler("/executable.exe", handleRequest); + + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://localhost:8000/save_filenames.html" + ); + + expectedItems = await getItems("items"); +}); + +function getItems(parentid) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [parentid, AppConstants.platform], + (id, platform) => { + let elements = []; + let elem = content.document.getElementById(id).firstElementChild; + while (elem) { + let filename = + elem.dataset["filenamePlatform" + platform] || elem.dataset.filename; + let url = elem.getAttribute("src"); + let draggable = + elem.localName == "img" && elem.dataset.nodrag != "true"; + let unknown = elem.dataset.unknown; + let noattach = elem.dataset.noattach; + let savepagename = elem.dataset.savepagename; + elements.push({ + draggable, + unknown, + filename, + url, + noattach, + savepagename, + }); + elem = elem.nextElementSibling; + } + return elements; + } + ); +} + +function getDirectoryEntries(dir) { + let files = []; + let entries = dir.directoryEntries; + while (true) { + let file = entries.nextFile; + if (!file) { + break; + } + files.push(file.leafName); + } + entries.close(); + return files; +} + +// This test saves the document as a complete web page and verifies +// that the resources are saved with the correct filename. +add_task(async function save_document() { + let browser = gBrowser.selectedBrowser; + + let tmp = SpecialPowers.Services.dirsvc.get("TmpD", Ci.nsIFile); + const baseFilename = "test_save_filenames_" + Date.now(); + + let tmpFile = tmp.clone(); + tmpFile.append(baseFilename + "_document.html"); + let tmpDir = tmp.clone(); + tmpDir.append(baseFilename + "_document_files"); + + MockFilePicker.displayDirectory = tmpDir; + MockFilePicker.showCallback = function (fp) { + MockFilePicker.setFiles([tmpFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + }; + + let downloadsList = await Downloads.getList(Downloads.PUBLIC); + let savePromise = new Promise((resolve, reject) => { + downloadsList.addView({ + onDownloadChanged(download) { + if (download.succeeded) { + downloadsList.removeView(this); + downloadsList.removeFinished(); + resolve(); + } + }, + }); + }); + saveBrowser(browser); + await savePromise; + + let filesSaved = getDirectoryEntries(tmpDir); + + for (let idx = 0; idx < expectedItems.length; idx++) { + let filename = expectedItems[idx].filename; + if (idx == 66 && AppConstants.platform == "win") { + // This is special-cased on Windows. The default filename will be used, since + // the filename is invalid, but since the previous test file has the same issue, + // this second file will be saved with a number suffix added to it. + filename = "Untitled_002"; + } + + let file = tmpDir.clone(); + file.append(filename); + + let fileIdx = -1; + // Use checkShortenedFilename to check long filenames. + if (filename.length > 240) { + for (let t = 0; t < filesSaved.length; t++) { + if ( + filesSaved[t].length > 60 && + checkShortenedFilename(filesSaved[t], filename) + ) { + fileIdx = t; + break; + } + } + } else { + fileIdx = filesSaved.indexOf(filename); + } + + ok( + fileIdx >= 0, + "file i" + + idx + + " " + + filename + + " was saved with the correct name using saveDocument" + ); + if (fileIdx >= 0) { + // If found, remove it from the list. At end of the test, the + // list should be empty. + filesSaved.splice(fileIdx, 1); + } + } + + is(filesSaved.length, 0, "all files accounted for"); + tmpDir.remove(true); + tmpFile.remove(false); +}); + +// This test simulates dragging the images in the document and ensuring that +// the correct filename is used for each one. +// On Mac, the data is added in the parent process instead, so we cannot +// test dragging directly. +if (AppConstants.platform != "macosx") { + add_task(async function drag_files() { + let browser = gBrowser.selectedBrowser; + + await SpecialPowers.spawn(browser, [PROMISE_FILENAME_TYPE], type => { + content.addEventListener("dragstart", event => { + content.draggedFile = event.dataTransfer.getData(type); + event.preventDefault(); + }); + }); + + for (let idx = 0; idx < expectedItems.length; idx++) { + if (!expectedItems[idx].draggable) { + // You can't drag non-images and invalid images. + continue; + } + + await BrowserTestUtils.synthesizeMouse( + "#i" + idx, + 1, + 1, + { type: "mousedown" }, + browser + ); + await BrowserTestUtils.synthesizeMouse( + "#i" + idx, + 11, + 11, + { type: "mousemove" }, + browser + ); + await BrowserTestUtils.synthesizeMouse( + "#i" + idx, + 20, + 20, + { type: "mousemove" }, + browser + ); + await BrowserTestUtils.synthesizeMouse( + "#i" + idx, + 20, + 20, + { type: "mouseup" }, + browser + ); + + let draggedFile = await SpecialPowers.spawn(browser, [], () => { + let file = content.draggedFile; + content.draggedFile = null; + return file; + }); + + is( + draggedFile, + expectedItems[idx].filename, + "i" + + idx + + " " + + expectedItems[idx].filename + + " was saved with the correct name when dragging" + ); + } + }); +} + +// This test checks that copying an image provides the right filename +// for pasting to the local file system. This is only implemented on Windows. +if (AppConstants.platform == "win") { + add_task(async function copy_image() { + for (let idx = 0; idx < expectedItems.length; idx++) { + if (!expectedItems[idx].draggable) { + // You can't context-click on non-images. + continue; + } + + let data = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [idx, PROMISE_FILENAME_TYPE], + (imagenum, type) => { + // No need to wait for the data to be really on the clipboard, we only + // need the promise data added when the command is performed. + SpecialPowers.setCommandNode( + content, + content.document.getElementById("i" + imagenum) + ); + SpecialPowers.doCommand(content, "cmd_copyImageContents"); + + return SpecialPowers.getClipboardData(type); + } + ); + + is( + data, + expectedItems[idx].filename, + "i" + + idx + + " " + + expectedItems[idx].filename + + " was saved with the correct name when copying" + ); + } + }); +} + +// This test checks the default filename selected when selecting to save +// a file from either the context menu or what would happen when save page +// as was selected from the file menu. Note that this tests a filename assigned +// when using content-disposition: inline. +add_task(async function saveas_files() { + // Iterate over each item and try saving them from the context menu, + // and the Save Page As command on the File menu. + for (let testname of ["context menu", "save page as"]) { + for (let idx = 0; idx < expectedItems.length; idx++) { + let menu; + if (testname == "context menu") { + if (!expectedItems[idx].draggable) { + // You can't context-click on non-images. + continue; + } + + menu = document.getElementById("contentAreaContextMenu"); + let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); + BrowserTestUtils.synthesizeMouse( + "#i" + idx, + 5, + 5, + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + await popupShown; + } else { + if (expectedItems[idx].unknown == "typeonly") { + // Items marked with unknown="typeonly" have unknown content types and + // will be downloaded instead of opened in a tab. + let list = await Downloads.getList(Downloads.PUBLIC); + let downloadFinishedPromise = promiseDownloadFinished(list); + + await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: expectedItems[idx].url, + waitForLoad: false, + waitForStateStop: true, + }); + + let download = await downloadFinishedPromise; + + let filename = PathUtils.filename(download.target.path); + + let expectedFilename = expectedItems[idx].filename; + if (expectedFilename.length > 240) { + ok( + checkShortenedFilename(filename, expectedFilename), + "open link" + + idx + + " " + + expectedFilename + + " was downloaded with the correct name when opened as a url (with long name)" + ); + } else { + is( + filename, + expectedFilename, + "open link" + + idx + + " " + + expectedFilename + + " was downloaded with the correct name when opened as a url" + ); + } + + try { + await IOUtils.remove(download.target.path); + } catch (ex) {} + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + continue; + } + + await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: expectedItems[idx].url, + waitForLoad: false, + waitForStateStop: true, + }); + } + + let filename = await new Promise(resolve => { + MockFilePicker.showCallback = function (fp) { + setTimeout(() => { + resolve(fp.defaultString); + }, 0); + return Ci.nsIFilePicker.returnCancel; + }; + + if (testname == "context menu") { + let menuitem = document.getElementById("context-saveimage"); + menu.activateItem(menuitem); + } else if (testname == "save page as") { + document.getElementById("Browser:SavePage").doCommand(); + } + }); + + // Trying to open an unknown or binary type will just open a blank + // page, so trying to save will just save the blank page with the + // filename Untitled.html. + let expectedFilename = expectedItems[idx].unknown + ? DEFAULT_FILENAME + : expectedItems[idx].savepagename || expectedItems[idx].filename; + + // When saving via contentAreaUtils.js, the content disposition name + // field is used as an alternate. + if (expectedFilename == "save_thename.png") { + expectedFilename = "withname.png"; + } + + is( + filename, + expectedFilename, + "i" + + idx + + " " + + expectedFilename + + " was saved with the correct name " + + testname + ); + + if (testname == "save page as") { + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + } + } +}); + +// This test checks that links that result in files with +// content-disposition: attachment are saved with the right filenames. +add_task(async function save_links() { + sendAsAttachment = true; + + // Create some links based on each image and insert them into the document. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + let doc = content.document; + let insertPos = doc.getElementById("attachment-links"); + + let idx = 0; + let elem = doc.getElementById("items").firstElementChild; + while (elem) { + let attachmentlink = doc.createElement("a"); + attachmentlink.id = "attachmentlink" + idx; + attachmentlink.href = elem.localName == "object" ? elem.data : elem.src; + attachmentlink.textContent = elem.dataset.filename; + insertPos.appendChild(attachmentlink); + insertPos.appendChild(doc.createTextNode(" ")); + + elem = elem.nextElementSibling; + idx++; + } + }); + + let list = await Downloads.getList(Downloads.PUBLIC); + + for (let idx = 0; idx < expectedItems.length; idx++) { + // Skip the items that won't have a content-disposition. + if (expectedItems[idx].noattach) { + continue; + } + + let downloadFinishedPromise = promiseDownloadFinished(list); + + BrowserTestUtils.synthesizeMouse( + "#attachmentlink" + idx, + 5, + 5, + {}, + gBrowser.selectedBrowser + ); + + let download = await downloadFinishedPromise; + + let filename = PathUtils.filename(download.target.path); + + let expectedFilename = expectedItems[idx].filename; + // Use checkShortenedFilename to check long filenames. + if (expectedItems[idx].filename.length > 240) { + ok( + checkShortenedFilename(filename, expectedFilename), + "attachmentlink" + + idx + + " " + + expectedFilename + + " was saved with the correct name when opened as attachment (with long name)" + ); + } else { + is( + filename, + expectedFilename, + "attachmentlink" + + idx + + " " + + expectedFilename + + " was saved with the correct name when opened as attachment" + ); + } + + try { + await IOUtils.remove(download.target.path); + } catch (ex) {} + } + + sendAsAttachment = false; +}); + +// This test checks some cases where links to images are saved using Save Link As, +// and when opening them in a new tab and then using Save Page As. +add_task(async function saveas_image_links() { + let links = await getItems("links"); + + // Iterate over each link and try saving the links from the context menu, + // and then after opening a new tab for that link and then selecting + // the Save Page As command on the File menu. + for (let testname of ["save link as", "save link then save page as"]) { + for (let idx = 0; idx < links.length; idx++) { + let menu = document.getElementById("contentAreaContextMenu"); + let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); + BrowserTestUtils.synthesizeMouse( + "#link" + idx, + 5, + 5, + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + await popupShown; + + let promptPromise = new Promise(resolve => { + MockFilePicker.showCallback = function (fp) { + setTimeout(() => { + resolve(fp.defaultString); + }, 0); + return Ci.nsIFilePicker.returnCancel; + }; + }); + + if (testname == "save link as") { + let menuitem = document.getElementById("context-savelink"); + menu.activateItem(menuitem); + } else { + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + + let menuitem = document.getElementById("context-openlinkintab"); + menu.activateItem(menuitem); + + let tab = await newTabPromise; + await BrowserTestUtils.switchTab(gBrowser, tab); + + document.getElementById("Browser:SavePage").doCommand(); + } + + let filename = await promptPromise; + + let expectedFilename = links[idx].filename; + // Only codepaths that go through contentAreaUtils.js use the + // name from the content disposition. + if (testname == "save link as" && expectedFilename == "four.png") { + expectedFilename = "save_filename.png"; + } + + is( + filename, + expectedFilename, + "i" + + idx + + " " + + expectedFilename + + " link was saved with the correct name " + + testname + ); + + if (testname == "save link then save page as") { + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + } + } +}); + +// This test checks that links that with a download attribute +// are saved with the right filenames. +add_task(async function save_download_links() { + let downloads = await getItems("downloads"); + + let list = await Downloads.getList(Downloads.PUBLIC); + for (let idx = 0; idx < downloads.length; idx++) { + let downloadFinishedPromise = promiseDownloadFinished(list); + + BrowserTestUtils.synthesizeMouse( + "#download" + idx, + 2, + 2, + {}, + gBrowser.selectedBrowser + ); + + let download = await downloadFinishedPromise; + + let filename = PathUtils.filename(download.target.path); + + if (downloads[idx].filename.length > 240) { + ok( + checkShortenedFilename(filename, downloads[idx].filename), + "download" + + idx + + " " + + downloads[idx].filename + + " was saved with the correct name when link has download attribute" + ); + } else { + if (idx == 66 && filename == "Untitled(1)") { + // Sometimes, the previous test's file still exists or wasn't created in time + // and a non-duplicated name is created. Allow this rather than figuring out + // how to avoid it since it doesn't affect what is being tested here. + filename = "Untitled"; + } + + is( + filename, + downloads[idx].filename, + "download" + + idx + + " " + + downloads[idx].filename + + " was saved with the correct name when link has download attribute" + ); + } + + try { + await IOUtils.remove(download.target.path); + } catch (ex) {} + } +}); + +// This test verifies that invalid extensions are not removed when they +// have been entered in the file picker. +add_task(async function save_page_with_invalid_after_filepicker() { + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://localhost:8000/save_filename.sjs?type=html&filename=invfile.lnk" + ); + + let filename = await new Promise(resolve => { + MockFilePicker.showCallback = function (fp) { + let expectedFilename = + AppConstants.platform == "win" ? "invfile.lnk.htm" : "invfile.lnk.html"; + is(fp.defaultString, expectedFilename, "supplied filename is correct"); + setTimeout(() => { + resolve("otherfile.local"); + }, 0); + return Ci.nsIFilePicker.returnCancel; + }; + + document.getElementById("Browser:SavePage").doCommand(); + }); + + is(filename, "otherfile.local", "lnk extension has been preserved"); + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function save_page_with_invalid_extension() { + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://localhost:8000/save_filename.sjs?type=html" + ); + + let filename = await new Promise(resolve => { + MockFilePicker.showCallback = function (fp) { + setTimeout(() => { + resolve(fp.defaultString); + }, 0); + return Ci.nsIFilePicker.returnCancel; + }; + + document.getElementById("Browser:SavePage").doCommand(); + }); + + is( + filename, + AppConstants.platform == "win" ? "file.inv.htm" : "file.inv.html", + "html extension has been added" + ); + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async () => { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + MockFilePicker.cleanup(); + await new Promise(resolve => httpServer.stop(resolve)); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_shows_where_to_save_dialog.js b/uriloader/exthandler/tests/mochitest/browser_shows_where_to_save_dialog.js new file mode 100644 index 0000000000..6bf375dfff --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_shows_where_to_save_dialog.js @@ -0,0 +1,303 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { DownloadIntegration } = ChromeUtils.importESModule( + "resource://gre/modules/DownloadIntegration.sys.mjs" +); + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +const { handleInternally, useHelperApp, useSystemDefault, saveToDisk } = + Ci.nsIHandlerInfo; + +let MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.useDownloadDir", false], + ], + }); + + registerCleanupFunction(async () => { + let hiddenPromise = BrowserTestUtils.waitForEvent( + DownloadsPanel.panel, + "popuphidden" + ); + DownloadsPanel.hidePanel(); + await hiddenPromise; + MockFilePicker.cleanup(); + }); +}); + +// This test ensures that a "Save as..." filepicker dialog is shown for a file +// if useDownloadDir ("Always ask where to save files") is set to false and +// the filetype is set to save to disk. +add_task(async function aDownloadSavedToDiskPromptsForFolder() { + let publicList = await Downloads.getList(Downloads.PUBLIC); + ensureMIMEState( + { preferredAction: saveToDisk }, + { type: "text/plain", ext: "txt" } + ); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + let filePickerShownPromise = new Promise(resolve => { + MockFilePicker.showCallback = function (fp) { + setTimeout(resolve, 0); + return Ci.nsIFilePicker.returnCancel; + }; + }); + + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "file_txt_attachment_test.txt", + waitForLoad: false, + waitForStateStop: true, + }); + + info("Waiting on filepicker."); + await filePickerShownPromise; + ok(true, "filepicker should have been shown"); + + BrowserTestUtils.removeTab(loadingTab); +}); + +// This test ensures that downloads configured to open internally create only +// one file destination when saved via the filepicker, and don't prompt. +add_task(async function testFilesHandledInternally() { + let dir = await setupFilePickerDirectory(); + + ensureMIMEState( + { preferredAction: handleInternally }, + { type: "image/webp", ext: "webp" } + ); + + let filePickerShown = false; + MockFilePicker.showCallback = function (fp) { + filePickerShown = true; + return Ci.nsIFilePicker.returnCancel; + }; + + let thirdTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + url => { + info("Got load for " + url); + return url.endsWith("file_green.webp") && url.startsWith("file:"); + }, + true, + true + ); + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "file_green.webp", + waitForLoad: false, + waitForStateStop: true, + }); + + let openedTab = await thirdTabPromise; + ok(!filePickerShown, "file picker should not have shown up."); + + assertCorrectFile(dir, "file_green.webp"); + + // Cleanup + BrowserTestUtils.removeTab(loadingTab); + BrowserTestUtils.removeTab(openedTab); +}); + +// This test ensures that downloads configured to open with a system default +// app create only one file destination and don't open the filepicker. +add_task(async function testFilesHandledBySystemDefaultApp() { + let dir = await setupFilePickerDirectory(); + + ensureMIMEState({ preferredAction: useSystemDefault }); + + let filePickerShown = false; + MockFilePicker.showCallback = function (fp) { + filePickerShown = true; + return Ci.nsIFilePicker.returnCancel; + }; + + let oldLaunchFile = DownloadIntegration.launchFile; + let launchFileCalled = new Promise(resolve => { + DownloadIntegration.launchFile = async (file, mimeInfo) => { + is( + useSystemDefault, + mimeInfo.preferredAction, + "The file should be launched with a system app handler." + ); + resolve(); + }; + }); + + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "file_pdf_application_pdf.pdf", + waitForLoad: false, + waitForStateStop: true, + }); + + await launchFileCalled; + ok(!filePickerShown, "file picker should not have shown up."); + + assertCorrectFile(dir, "file_pdf_application_pdf.pdf"); + + // Cleanup + BrowserTestUtils.removeTab(loadingTab); + DownloadIntegration.launchFile = oldLaunchFile; +}); + +// This test ensures that downloads configured to open with a helper app create +// only one file destination when saved via the filepicker. +add_task(async function testFilesHandledByHelperApp() { + let dir = await setupFilePickerDirectory(); + + // Create a custom helper app so we can check that a launcherPath is + // configured for the serialized download. + let appHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + appHandler.name = "Dummy Test Handler"; + appHandler.executable = Services.dirsvc.get("ProfD", Ci.nsIFile); + appHandler.executable.append("helper_handler_test.exe"); + + if (!appHandler.executable.exists()) { + appHandler.executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o777); + } + + ensureMIMEState({ + preferredAction: useHelperApp, + preferredHandlerApp: appHandler, + }); + + let filePickerShown = false; + MockFilePicker.showCallback = function (fp) { + filePickerShown = true; + return Ci.nsIFilePicker.returnCancel; + }; + + let publicDownloads = await Downloads.getList(Downloads.PUBLIC); + let downloadFinishedPromise = new Promise(resolve => { + publicDownloads.addView({ + onDownloadChanged(download) { + if (download.succeeded || download.error) { + ok( + download.launcherPath.includes("helper_handler_test.exe"), + "Launcher path is available." + ); + resolve(); + } + }, + }); + }); + + let oldLaunchFile = DownloadIntegration.launchFile; + let launchFileCalled = new Promise(resolve => { + DownloadIntegration.launchFile = async (file, mimeInfo) => { + is( + useHelperApp, + mimeInfo.preferredAction, + "The file should be launched with a helper app handler." + ); + resolve(); + }; + }); + + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "file_pdf_application_pdf.pdf", + waitForLoad: false, + waitForStateStop: true, + }); + + await downloadFinishedPromise; + await launchFileCalled; + ok(!filePickerShown, "file picker should not have shown up."); + assertCorrectFile(dir, "file_pdf_application_pdf.pdf"); + + // Cleanup + BrowserTestUtils.removeTab(loadingTab); + DownloadIntegration.launchFile = oldLaunchFile; +}); + +async function setupFilePickerDirectory() { + let saveDir = createSaveDir(); + Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, saveDir); + Services.prefs.setIntPref("browser.download.folderList", 2); + + MockFilePicker.displayDirectory = saveDir; + MockFilePicker.returnValue = MockFilePicker.returnOK; + MockFilePicker.showCallback = function (fp) { + let file = saveDir.clone(); + file.append(fp.defaultString); + MockFilePicker.setFiles([file]); + }; + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.download.dir"); + Services.prefs.clearUserPref("browser.download.folderList"); + let publicList = await Downloads.getList(Downloads.PUBLIC); + let unfinishedDownloads = new Set( + (await publicList.getAll()).filter(dl => !dl.succeeded && !dl.error) + ); + if (unfinishedDownloads.size) { + info(`Have ${unfinishedDownloads.size} unfinished downloads, waiting.`); + await new Promise(resolve => { + let view = { + onChanged(dl) { + if (unfinishedDownloads.has(dl) && (dl.succeeded || dl.error)) { + unfinishedDownloads.delete(dl); + info(`Removed another download.`); + if (!unfinishedDownloads.size) { + publicList.removeView(view); + resolve(); + } + } + }, + }; + publicList.addView(view); + }); + } + try { + await IOUtils.remove(saveDir.path, { recursive: true }); + } catch (e) { + console.error(e); + } + }); + + return saveDir; +} + +function assertCorrectFile(saveDir, filename) { + info("Make sure additional files haven't been created."); + let iter = saveDir.directoryEntries; + let file = iter.nextFile; + ok(file.path.includes(filename), "Download has correct filename"); + ok(!iter.nextFile, "Only one file was created."); +} + +function createSaveDir() { + info("Creating save directory."); + let time = new Date().getTime(); + let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append(time); + return saveDir; +} + +function ensureMIMEState( + { preferredAction, preferredHandlerApp = null }, + { type = "application/pdf", ext = "pdf" } = {} +) { + const mimeInfo = gMimeSvc.getFromTypeAndExtension(type, ext); + mimeInfo.preferredAction = preferredAction; + mimeInfo.preferredApplicationHandler = preferredHandlerApp; + mimeInfo.alwaysAskBeforeHandling = false; + gHandlerSvc.store(mimeInfo); +} diff --git a/uriloader/exthandler/tests/mochitest/browser_txt_download_save_as.js b/uriloader/exthandler/tests/mochitest/browser_txt_download_save_as.js new file mode 100644 index 0000000000..7155c35fd9 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_txt_download_save_as.js @@ -0,0 +1,168 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { DownloadIntegration } = ChromeUtils.importESModule( + "resource://gre/modules/DownloadIntegration.sys.mjs" +); +const HandlerService = Cc[ + "@mozilla.org/uriloader/handler-service;1" +].getService(Ci.nsIHandlerService); +const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +const { + saveToDisk, + alwaysAsk, + handleInternally, + useHelperApp, + useSystemDefault, +} = Ci.nsIHandlerInfo; +const testDir = createTemporarySaveDirectory(); +const MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this +); + +async function testSaveAsDownload() { + await BrowserTestUtils.withNewTab( + `data:text/html,Test TXT Link`, + async browser => { + let menu = document.getElementById("contentAreaContextMenu"); + ok(menu, "Context menu exists on the page"); + + let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); + BrowserTestUtils.synthesizeMouseAtCenter( + "a#test-link", + { type: "contextmenu", button: 2 }, + browser + ); + await popupShown; + info("Context menu popup was successfully displayed"); + + let filePickerPromise = setupFilePicker(); + + info("Clicking Save As... context menu"); + let menuitem = menu.querySelector("#context-savelink"); + menu.activateItem(menuitem); + await filePickerPromise; + } + ); +} + +async function setupFilePicker() { + return new Promise(resolve => { + MockFilePicker.returnValue = MockFilePicker.returnOK; + MockFilePicker.displayDirectory = testDir; + MockFilePicker.showCallback = fp => { + ok(true, "filepicker should be visible"); + ok( + fp.defaultExtension === "txt", + "Default extension in filepicker should be txt" + ); + ok( + fp.defaultString === "file_txt_attachment_test.txt", + "Default string name in filepicker should have the correct file name" + ); + const destFile = testDir.clone(); + destFile.append(fp.defaultString); + MockFilePicker.setFiles([destFile]); + + mockTransferCallback = success => { + ok(success, "File should have been downloaded successfully"); + ok(destFile.exists(), "File should exist in test directory"); + resolve(destFile); + }; + }; + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.useDownloadDir", false], + ], + }); + mockTransferRegisterer.register(); + + let oldLaunchFile = DownloadIntegration.launchFile; + DownloadIntegration.launchFile = () => { + ok(false, "Download should not have launched"); + }; + + registerCleanupFunction(async () => { + DownloadIntegration.launchFile = oldLaunchFile; + mockTransferRegisterer.unregister(); + + // We only want to run MockFilerPicker.cleanup after the entire test is run. + // Otherwise, we cannot use MockFilePicker for each preferredAction. + MockFilePicker.cleanup(); + + testDir.remove(true); + ok(!testDir.exists(), "Test directory should be removed"); + }); +}); + +/** + * Tests that selecting the context menu item `Save Link As…` on a txt file link + * opens the file picker and only downloads the file without any launches when + * browser.download.always_ask_before_handling_new_types is disabled. + */ +add_task(async function test_txt_save_as_link() { + let mimeInfo; + + for (let preferredAction of [ + saveToDisk, + alwaysAsk, + handleInternally, + useHelperApp, + useSystemDefault, + ]) { + mimeInfo = MIMEService.getFromTypeAndExtension("text/plain", "txt"); + mimeInfo.alwaysAskBeforeHandling = preferredAction === alwaysAsk; + mimeInfo.preferredAction = preferredAction; + HandlerService.store(mimeInfo); + + info( + `Setting up filepicker with preferredAction ${preferredAction} and ask = ${mimeInfo.alwaysAskBeforeHandling}` + ); + await testSaveAsDownload(mimeInfo); + } +}); + +/** + * Tests that selecting the context menu item `Save Link As…` on a txt file link + * opens the file picker and only downloads the file without any launches when + * browser.download.always_ask_before_handling_new_types is disabled. For this + * particular test, set alwaysAskBeforeHandling to true. + */ +add_task(async function test_txt_save_as_link_alwaysAskBeforeHandling() { + let mimeInfo; + + for (let preferredAction of [ + saveToDisk, + alwaysAsk, + handleInternally, + useHelperApp, + useSystemDefault, + ]) { + mimeInfo = MIMEService.getFromTypeAndExtension("text/plain", "txt"); + mimeInfo.alwaysAskBeforeHandling = true; + mimeInfo.preferredAction = preferredAction; + HandlerService.store(mimeInfo); + + info( + `Setting up filepicker with preferredAction ${preferredAction} and ask = ${mimeInfo.alwaysAskBeforeHandling}` + ); + await testSaveAsDownload(mimeInfo); + } +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_web_handler_app_pinned_tab.js b/uriloader/exthandler/tests/mochitest/browser_web_handler_app_pinned_tab.js new file mode 100644 index 0000000000..2f06833665 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_web_handler_app_pinned_tab.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let testURL = + "http://mochi.test:8888/browser/" + + "uriloader/exthandler/tests/mochitest/mailto.html"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "gExternalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "gHandlerService", + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService" +); + +let prevAlwaysAskBeforeHandling; +let prevPreferredAction; +let prevPreferredApplicationHandler; + +add_setup(async function () { + let handler = gExternalProtocolService.getProtocolHandlerInfo("mailto", {}); + + // Create a fake mail handler + const APP_NAME = "ExMail"; + const HANDLER_URL = "https://example.com/?extsrc=mailto&url=%s"; + let app = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + app.uriTemplate = HANDLER_URL; + app.name = APP_NAME; + + // Store defaults + prevAlwaysAskBeforeHandling = handler.alwaysAskBeforeHandling; + prevPreferredAction = handler.preferredAction; + prevPreferredApplicationHandler = handler.preferredApplicationHandler; + + // Set the fake app as default + handler.alwaysAskBeforeHandling = false; + handler.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + handler.preferredApplicationHandler = app; + gHandlerService.store(handler); +}); + +registerCleanupFunction(async function () { + let handler = gExternalProtocolService.getProtocolHandlerInfo("mailto", {}); + handler.alwaysAskBeforeHandling = prevAlwaysAskBeforeHandling; + handler.preferredAction = prevPreferredAction; + handler.preferredApplicationHandler = prevPreferredApplicationHandler; + gHandlerService.store(handler); +}); + +add_task(async function () { + const expectedURL = + "https://example.com/?extsrc=mailto&url=mailto%3Amail%40example.com"; + + // Load a page with mailto handler. + let browser = gBrowser.selectedBrowser; + BrowserTestUtils.loadURIString(browser, testURL); + await BrowserTestUtils.browserLoaded(browser, false, testURL); + + // Pin as an app tab + gBrowser.pinTab(gBrowser.selectedTab); + + // Click the link and check the new tab is correct + let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL); + await BrowserTestUtils.synthesizeMouseAtCenter("#link", {}, browser); + let tab = await promiseTabOpened; + is( + gURLBar.value, + expectedURL, + "the mailto web handler is opened in a new tab" + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js new file mode 100644 index 0000000000..8046629219 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js @@ -0,0 +1,124 @@ +let testURL = + "https://example.com/browser/" + + "uriloader/exthandler/tests/mochitest/protocolHandler.html"; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["security.external_protocol_requires_permission", false]], + }); + + // Load a page registering a protocol handler. + let browser = gBrowser.selectedBrowser; + BrowserTestUtils.loadURIString(browser, testURL); + await BrowserTestUtils.browserLoaded(browser, false, testURL); + + // Register the protocol handler by clicking the notificationbar button. + let notificationValue = "Protocol Registration: web+testprotocol"; + let getNotification = () => + gBrowser.getNotificationBox().getNotificationWithValue(notificationValue); + await BrowserTestUtils.waitForCondition(getNotification); + let notification = getNotification(); + let button = notification.buttonContainer.querySelector("button"); + ok(button, "got registration button"); + button.click(); + + // Set the new handler as default. + const protoSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + let protoInfo = protoSvc.getProtocolHandlerInfo("web+testprotocol"); + is( + protoInfo.preferredAction, + protoInfo.useHelperApp, + "using a helper application is the preferred action" + ); + ok(!protoInfo.preferredApplicationHandler, "no preferred handler is set"); + let handlers = protoInfo.possibleApplicationHandlers; + is(1, handlers.length, "only one handler registered for web+testprotocol"); + let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp); + ok(handler instanceof Ci.nsIWebHandlerApp, "the handler is a web handler"); + is( + handler.uriTemplate, + "https://example.com/foobar?uri=%s", + "correct url template" + ); + protoInfo.preferredApplicationHandler = handler; + protoInfo.alwaysAskBeforeHandling = false; + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + handlerSvc.store(protoInfo); + + const expectedURL = + "https://example.com/foobar?uri=web%2Btestprotocol%3Atest"; + + // Create a framed link: + await SpecialPowers.spawn(browser, [], async function () { + let iframe = content.document.createElement("iframe"); + iframe.src = `data:text/html,Click me`; + content.document.body.append(iframe); + // Can't return this promise because it resolves to the event object. + await ContentTaskUtils.waitForEvent(iframe, "load"); + iframe.contentDocument.querySelector("a").click(); + }); + let kidContext = browser.browsingContext.children[0]; + await TestUtils.waitForCondition(() => { + let spec = kidContext.currentWindowGlobal?.documentURI?.spec || ""; + return spec == expectedURL; + }); + is( + kidContext.currentWindowGlobal.documentURI.spec, + expectedURL, + "Should load in frame." + ); + + // Middle-click a testprotocol link and check the new tab is correct + let link = "#link"; + + let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL); + await BrowserTestUtils.synthesizeMouseAtCenter(link, { button: 1 }, browser); + let tab = await promiseTabOpened; + gBrowser.selectedTab = tab; + is( + gURLBar.value, + expectedURL, + "the expected URL is displayed in the location bar" + ); + BrowserTestUtils.removeTab(tab); + + // Shift-click the testprotocol link and check the new window. + let newWindowPromise = BrowserTestUtils.waitForNewWindow({ + url: expectedURL, + }); + await BrowserTestUtils.synthesizeMouseAtCenter( + link, + { shiftKey: true }, + browser + ); + let win = await newWindowPromise; + await BrowserTestUtils.waitForCondition( + () => win.gBrowser.currentURI.spec == expectedURL + ); + is( + win.gURLBar.value, + expectedURL, + "the expected URL is displayed in the location bar" + ); + await BrowserTestUtils.closeWindow(win); + + // Click the testprotocol link and check the url in the current tab. + let loadPromise = BrowserTestUtils.browserLoaded(browser); + await BrowserTestUtils.synthesizeMouseAtCenter(link, {}, browser); + await loadPromise; + await BrowserTestUtils.waitForCondition(() => gURLBar.value != testURL); + is( + gURLBar.value, + expectedURL, + "the expected URL is displayed in the location bar" + ); + + // Cleanup. + protoInfo.preferredApplicationHandler = null; + handlers.removeElementAt(0); + handlerSvc.store(protoInfo); +}); diff --git a/uriloader/exthandler/tests/mochitest/download.bin b/uriloader/exthandler/tests/mochitest/download.bin new file mode 100644 index 0000000000..0e4b0c7bae --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/download.bin @@ -0,0 +1 @@ +abc123 diff --git a/uriloader/exthandler/tests/mochitest/download.sjs b/uriloader/exthandler/tests/mochitest/download.sjs new file mode 100644 index 0000000000..d02d2b7355 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/download.sjs @@ -0,0 +1,42 @@ +"use strict"; + +function actuallyHandleRequest(req, res) { + res.setHeader("Content-Type", "application/octet-stream", false); + res.write("abc123"); + res.finish(); +} + +function handleRequest(req, res) { + if (req.queryString.includes("finish")) { + res.write("OK"); + let downloadReq = null; + getObjectState("downloadReq", o => { + downloadReq = o; + }); + // Two possibilities: either the download request has already reached us, or not. + if (downloadReq) { + downloadReq.wrappedJSObject.callback(); + } else { + // Set a variable to allow the request to complete immediately: + setState("finishReq", "true"); + } + } else if (req.queryString.includes("reset")) { + res.write("OK"); + setObjectState("downloadReq", null); + setState("finishReq", "false"); + } else { + res.processAsync(); + if (getState("finishReq") === "true") { + actuallyHandleRequest(req, res); + } else { + let o = { + callback() { + actuallyHandleRequest(req, res); + }, + }; + o.wrappedJSObject = o; + o.QueryInterface = () => o; + setObjectState("downloadReq", o); + } + } +} diff --git a/uriloader/exthandler/tests/mochitest/download_page.html b/uriloader/exthandler/tests/mochitest/download_page.html new file mode 100644 index 0000000000..5a264888fa --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/download_page.html @@ -0,0 +1,22 @@ + + + + + + Test page for link clicking + + + + regular load + target blank + new window + click to reopen + target blank (noopener) + click to reopen (noopener) + new window (noopener) + diff --git a/uriloader/exthandler/tests/mochitest/file_as.exe b/uriloader/exthandler/tests/mochitest/file_as.exe new file mode 100644 index 0000000000..f2f5ab47f3 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_as.exe @@ -0,0 +1 @@ +Not actually an executable... but let's pretend! diff --git a/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ b/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ new file mode 100644 index 0000000000..89f22e30be --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ @@ -0,0 +1,2 @@ +Content-Type: binary/octet-stream +Content-Disposition: attachment diff --git a/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html b/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html new file mode 100644 index 0000000000..eb2fb74441 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html @@ -0,0 +1 @@ + diff --git a/uriloader/exthandler/tests/mochitest/file_green.webp b/uriloader/exthandler/tests/mochitest/file_green.webp new file mode 100644 index 0000000000..04b7f003b4 Binary files /dev/null and b/uriloader/exthandler/tests/mochitest/file_green.webp differ diff --git a/uriloader/exthandler/tests/mochitest/file_green.webp^headers^ b/uriloader/exthandler/tests/mochitest/file_green.webp^headers^ new file mode 100644 index 0000000000..3f6afd6625 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_green.webp^headers^ @@ -0,0 +1,3 @@ +Content-Disposition: attachment; filename=file_green.webp +Content-Type: image/webp + diff --git a/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg b/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg new file mode 100644 index 0000000000..b730c4c492 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg @@ -0,0 +1,3 @@ + + + diff --git a/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg^headers^ b/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg^headers^ new file mode 100644 index 0000000000..5279ae8636 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg^headers^ @@ -0,0 +1,2 @@ +content-disposition: attachment; filename=file_image_svgxml_svg; filename*=UTF-8''file_image_svgxml.svg +content-type: image/svg+xml diff --git a/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html b/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html new file mode 100644 index 0000000000..b1bb863f89 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html @@ -0,0 +1 @@ + diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf^headers^ new file mode 100644 index 0000000000..9e8cb41cba --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment; filename="file_pdf_application_octet_stream.pdf"; filename*=UTF-8''file_pdf_application_octet_stream.pdf +Content-Type: application/octet-stream diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ new file mode 100644 index 0000000000..d1d59b9754 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ @@ -0,0 +1,2 @@ +content-disposition: attachment; filename=file_pdf_application_pdf.pdf; filename*=UTF-8''file_pdf_application_pdf.pdf +content-type: application/pdf diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ new file mode 100644 index 0000000000..157c0e0943 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ @@ -0,0 +1,2 @@ +content-disposition: attachment; filename=file_pdf_application_unknown.pdf; filename*=UTF-8''file_pdf_application_unknown.pdf +content-type: application/unknown diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ new file mode 100644 index 0000000000..6358f54f48 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment; filename="file_pdf_binary_octet_stream.pdf"; filename*=UTF-8''file_pdf_binary_octet_stream.pdf +Content-Type: binary/octet-stream diff --git a/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ new file mode 100644 index 0000000000..dcfed6af23 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment; filename=file_txt_attachment_test.txt +Content-Type: text/plain diff --git a/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png new file mode 100644 index 0000000000..743292dc6f Binary files /dev/null and b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png differ diff --git a/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ new file mode 100644 index 0000000000..06e0cd957f --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: inline; filename=file_with%40%40funny_name.png +Content-Type: image/png diff --git a/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm new file mode 100644 index 0000000000..7bc738b8b4 Binary files /dev/null and b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm differ diff --git a/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ new file mode 100644 index 0000000000..b77e9d3687 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: inline; filename=file_with%5Bfunny_name.webm +Content-Type: video/webm diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml new file mode 100644 index 0000000000..3a5792586a --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml @@ -0,0 +1,4 @@ + + + + diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ new file mode 100644 index 0000000000..5bdc4448e8 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment +Content-Type: binary/octet-stream diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml new file mode 100644 index 0000000000..3a5792586a --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ new file mode 100644 index 0000000000..ac0355d976 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment; filename=file_xml_attachment_test.xml +Content-Type: text/xml diff --git a/uriloader/exthandler/tests/mochitest/head.js b/uriloader/exthandler/tests/mochitest/head.js new file mode 100644 index 0000000000..183aeee20e --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/head.js @@ -0,0 +1,535 @@ +var { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +var { HandlerServiceTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/HandlerServiceTestUtils.sys.mjs" +); + +var gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); +var gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +function createMockedHandlerApp() { + // Mock the executable + let mockedExecutable = FileUtils.getFile("TmpD", ["mockedExecutable"]); + if (!mockedExecutable.exists()) { + mockedExecutable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755); + } + + // Mock the handler app + let mockedHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + mockedHandlerApp.executable = mockedExecutable; + mockedHandlerApp.detailedDescription = "Mocked handler app"; + + registerCleanupFunction(function () { + // remove the mocked executable from disk. + if (mockedExecutable.exists()) { + mockedExecutable.remove(true); + } + }); + + return mockedHandlerApp; +} + +function createMockedObjects(createHandlerApp) { + // Mock the mime info + let internalMockedMIME = gMimeSvc.getFromTypeAndExtension( + "text/x-test-handler", + null + ); + internalMockedMIME.alwaysAskBeforeHandling = true; + internalMockedMIME.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + internalMockedMIME.appendExtension("abc"); + if (createHandlerApp) { + let mockedHandlerApp = createMockedHandlerApp(); + internalMockedMIME.description = mockedHandlerApp.detailedDescription; + internalMockedMIME.possibleApplicationHandlers.appendElement( + mockedHandlerApp + ); + internalMockedMIME.preferredApplicationHandler = mockedHandlerApp; + } + + // Proxy for the mocked MIME info for faking the read-only attributes + let mockedMIME = new Proxy(internalMockedMIME, { + get(target, property) { + switch (property) { + case "hasDefaultHandler": + return true; + case "defaultDescription": + return "Default description"; + default: + return target[property]; + } + }, + }); + + // Mock the launcher: + let mockedLauncher = { + MIMEInfo: mockedMIME, + source: Services.io.newURI("http://www.mozilla.org/"), + suggestedFileName: "test_download_dialog.abc", + targetFileIsExecutable: false, + saveToDisk() {}, + cancel() {}, + setDownloadToLaunch() {}, + setWebProgressListener() {}, + saveDestinationAvailable() {}, + contentLength: 42, + targetFile: null, // never read + // PRTime is microseconds since epoch, Date.now() returns milliseconds: + timeDownloadStarted: Date.now() * 1000, + QueryInterface: ChromeUtils.generateQI([ + "nsICancelable", + "nsIHelperAppLauncher", + ]), + }; + + registerCleanupFunction(function () { + // remove the mocked mime info from database. + let mockHandlerInfo = gMimeSvc.getFromTypeAndExtension( + "text/x-test-handler", + null + ); + if (gHandlerSvc.exists(mockHandlerInfo)) { + gHandlerSvc.remove(mockHandlerInfo); + } + }); + + return mockedLauncher; +} + +function createTemporarySaveDirectory() { + var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + return saveDir; +} + +async function openHelperAppDialog(launcher) { + let helperAppDialog = Cc[ + "@mozilla.org/helperapplauncherdialog;1" + ].createInstance(Ci.nsIHelperAppLauncherDialog); + + let helperAppDialogShownPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + try { + helperAppDialog.show(launcher, window, "foopy"); + } catch (ex) { + ok( + false, + "Trying to show unknownContentType.xhtml failed with exception: " + ex + ); + console.error(ex); + } + let dlg = await helperAppDialogShownPromise; + + is( + dlg.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Got correct dialog" + ); + + return dlg; +} + +/** + * Wait for a subdialog event indicating a dialog either opened + * or was closed. + * + * First argument is the browser in which to listen. If a tabbrowser, + * we listen to subdialogs for any tab of that browser. + */ +async function waitForSubDialog(browser, url, state) { + let eventStr = state ? "dialogopen" : "dialogclose"; + + let eventTarget; + + // Tabbrowser? + if (browser.tabContainer) { + eventTarget = browser.tabContainer.ownerDocument.documentElement; + } else { + // Individual browser. Get its box: + let tabDialogBox = browser.ownerGlobal.gBrowser.getTabDialogBox(browser); + eventTarget = tabDialogBox.getTabDialogManager()._dialogStack; + } + + let checkFn; + + if (state) { + checkFn = dialogEvent => dialogEvent.detail.dialog?._openedURL == url; + } + + let event = await BrowserTestUtils.waitForEvent( + eventTarget, + eventStr, + true, + checkFn + ); + + let { dialog } = event.detail; + + // If the dialog is closing wait for it to be fully closed before resolving + if (!state) { + await dialog._closingPromise; + } + + return event.detail.dialog; +} + +/** + * Wait for protocol permission dialog open/close. + * @param {MozBrowser} browser - Browser element the dialog belongs to. + * @param {boolean} state - true: dialog open, false: dialog close + * @returns {Promise} - Returns a promise which resolves with the + * SubDialog object of the dialog which closed or opened. + */ +async function waitForProtocolPermissionDialog(browser, state) { + return waitForSubDialog( + browser, + "chrome://mozapps/content/handling/permissionDialog.xhtml", + state + ); +} + +/** + * Wait for protocol app chooser dialog open/close. + * @param {MozBrowser} browser - Browser element the dialog belongs to. + * @param {boolean} state - true: dialog open, false: dialog close + * @returns {Promise} - Returns a promise which resolves with the + * SubDialog object of the dialog which closed or opened. + */ +async function waitForProtocolAppChooserDialog(browser, state) { + return waitForSubDialog( + browser, + "chrome://mozapps/content/handling/appChooser.xhtml", + state + ); +} + +async function promiseDownloadFinished(list, stopFromOpening) { + return new Promise(resolve => { + list.addView({ + onDownloadChanged(download) { + if (stopFromOpening) { + download.launchWhenSucceeded = false; + } + info("Download changed!"); + if (download.succeeded || download.error) { + info("Download succeeded or errored"); + list.removeView(this); + resolve(download); + } + }, + }); + }); +} + +function setupMailHandler() { + let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + let gOldMailHandlers = []; + + // Remove extant web handlers because they have icons that + // we fetch from the web, which isn't allowed in tests. + let handlers = mailHandlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + gOldMailHandlers.push(handler); + // If we get here, this is a web handler app. Remove it: + handlers.removeElementAt(i); + } catch (ex) {} + } + + let previousHandling = mailHandlerInfo.alwaysAskBeforeHandling; + mailHandlerInfo.alwaysAskBeforeHandling = true; + + // Create a dummy web mail handler so we always know the mailto: protocol. + // Without this, the test fails on VMs without a default mailto: handler, + // because no dialog is ever shown, as we ignore subframe navigations to + // protocols that cannot be handled. + let dummy = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + dummy.name = "Handler 1"; + dummy.uriTemplate = "https://example.com/first/%s"; + mailHandlerInfo.possibleApplicationHandlers.appendElement(dummy); + + gHandlerSvc.store(mailHandlerInfo); + registerCleanupFunction(() => { + // Re-add the original protocol handlers: + let mailHandlers = mailHandlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + // See if this is a web handler. If it is, it'll throw, otherwise, + // we will remove it. + mailHandlers.queryElementAt(i, Ci.nsIWebHandlerApp); + mailHandlers.removeElementAt(i); + } catch (ex) {} + } + for (let h of gOldMailHandlers) { + mailHandlers.appendElement(h); + } + mailHandlerInfo.alwaysAskBeforeHandling = previousHandling; + gHandlerSvc.store(mailHandlerInfo); + }); +} + +let gDownloadDir; + +async function setDownloadDir() { + let tmpDir = PathUtils.join( + PathUtils.tempDir, + "testsavedir" + Math.floor(Math.random() * 2 ** 32) + ); + // Create this dir if it doesn't exist (ignores existing dirs) + await IOUtils.makeDirectory(tmpDir); + registerCleanupFunction(async function () { + Services.prefs.clearUserPref("browser.download.folderList"); + Services.prefs.clearUserPref("browser.download.dir"); + try { + await IOUtils.remove(tmpDir, { recursive: true }); + } catch (e) { + console.error(e); + } + }); + Services.prefs.setIntPref("browser.download.folderList", 2); + Services.prefs.setCharPref("browser.download.dir", tmpDir); + return tmpDir; +} + +add_setup(async function test_common_initialize() { + gDownloadDir = await setDownloadDir(); + Services.prefs.setCharPref("browser.download.loglevel", "Debug"); + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.download.loglevel"); + }); +}); + +async function removeAllDownloads() { + let publicList = await Downloads.getList(Downloads.PUBLIC); + let downloads = await publicList.getAll(); + for (let download of downloads) { + await publicList.remove(download); + if (await IOUtils.exists(download.target.path)) { + await download.finalize(true); + } + } +} + +// Helpers for external protocol sandbox tests. +const EXT_PROTO_URI_MAILTO = "mailto:test@example.com"; + +/** + * Creates and iframe and navigate to an external protocol from the iframe. + * @param {MozBrowser} browser - Browser to spawn iframe in. + * @param {string} sandboxAttr - Sandbox attribute value for the iframe. + * @param {'trustedClick'|'untrustedClick'|'trustedLocationAPI'|'untrustedLocationAPI'|'frameSrc'|'frameSrcRedirect'} triggerMethod + * - How to trigger the navigation to the external protocol. + */ +async function navigateExternalProtoFromIframe( + browser, + sandboxAttr, + useCSPSandbox = false, + triggerMethod = "trustedClick" +) { + if ( + ![ + "trustedClick", + "untrustedClick", + "trustedLocationAPI", + "untrustedLocationAPI", + "frameSrc", + "frameSrcRedirect", + ].includes(triggerMethod) + ) { + throw new Error("Invalid trigger method " + triggerMethod); + } + + // Construct the url to use as iframe src. + let testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ); + let frameSrc = testPath + "/protocol_custom_sandbox_helper.sjs"; + + // Load the external protocol directly via the frame src field. + if (triggerMethod == "frameSrc") { + frameSrc = EXT_PROTO_URI_MAILTO; + } else if (triggerMethod == "frameSrcRedirect") { + let url = new URL(frameSrc); + url.searchParams.set("redirectCustomProtocol", "true"); + frameSrc = url.href; + } + + // If enabled set the sandbox attributes via CSP header instead. To do + // this we need to pass the sandbox flags to the test server via query + // params. + if (useCSPSandbox) { + let url = new URL(frameSrc); + url.searchParams.set("cspSandbox", sandboxAttr); + frameSrc = url.href; + + // If we use CSP sandbox attributes we shouldn't set any via iframe attribute. + sandboxAttr = null; + } + + // Create a sandboxed iframe and navigate to the external protocol. + await SpecialPowers.spawn( + browser, + [sandboxAttr, frameSrc, EXT_PROTO_URI_MAILTO, triggerMethod], + async (sandbox, src, extProtoURI, trigger) => { + let frame = content.document.createElement("iframe"); + + if (sandbox != null) { + frame.sandbox = sandbox; + } + + frame.src = src; + + let useFrameSrc = trigger == "frameSrc" || trigger == "frameSrcRedirect"; + + // Create frame load promise. + let frameLoadPromise; + // We won't get a load event if we directly put the external protocol in + // the frame src. + if (!useFrameSrc) { + frameLoadPromise = ContentTaskUtils.waitForEvent(frame, "load", false); + } + + content.document.body.appendChild(frame); + await frameLoadPromise; + + if (!useFrameSrc) { + // Trigger the external protocol navigation in the iframe. We test + // navigation by clicking links and navigation via the history API. + await SpecialPowers.spawn( + frame, + [extProtoURI, trigger], + async (uri, trigger2) => { + let link = content.document.createElement("a"); + link.innerText = "CLICK ME"; + link.id = "extProtoLink"; + content.document.body.appendChild(link); + + if (trigger2 == "trustedClick" || trigger2 == "untrustedClick") { + link.href = uri; + } else if ( + trigger2 == "trustedLocationAPI" || + trigger2 == "untrustedLocationAPI" + ) { + link.setAttribute("onclick", `location.href = '${uri}'`); + } + + if ( + trigger2 == "untrustedClick" || + trigger2 == "untrustedLocationAPI" + ) { + link.click(); + } else if ( + trigger2 == "trustedClick" || + trigger2 == "trustedLocationAPI" + ) { + await ContentTaskUtils.waitForCondition( + () => link, + "wait for link to be present" + ); + await EventUtils.synthesizeMouseAtCenter(link, {}, content); + } + } + ); + } + } + ); +} + +/** + * Wait for the sandbox error message which is shown in the web console when an + * external protocol navigation from a sandboxed context is blocked. + * @returns {Promise} - Promise which resolves once message has been logged. + */ +function waitForExtProtocolSandboxError() { + return new Promise(resolve => { + Services.console.registerListener(function onMessage(msg) { + let { message, logLevel } = msg; + if (logLevel != Ci.nsIConsoleMessage.error) { + return; + } + if ( + !message.includes( + `Blocked navigation to custom protocol “${EXT_PROTO_URI_MAILTO}” from a sandboxed context.` + ) + ) { + return; + } + Services.console.unregisterListener(onMessage); + resolve(); + }); + }); +} + +/** + * Run the external protocol sandbox test using iframes. + * @param {Object} options + * @param {boolean} options.blocked - Whether the navigation should be blocked. + * @param {string} options.sandbox - See {@link navigateExternalProtoFromIframe}. + * @param {string} options.useCSPSandbox - See {@link navigateExternalProtoFromIframe}. + * @param {string} options.triggerMethod - See {@link navigateExternalProtoFromIframe}. + * @returns {Promise} - Promise which resolves once the test has finished. + */ +function runExtProtocolSandboxTest(options) { + let { blocked, sandbox, useCSPSandbox = false, triggerMethod } = options; + + let testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ); + + info("runSandboxTest options: " + JSON.stringify(options)); + return BrowserTestUtils.withNewTab( + testPath + "/protocol_custom_sandbox_helper.sjs", + async browser => { + if (blocked) { + let errorPromise = waitForExtProtocolSandboxError(); + await navigateExternalProtoFromIframe( + browser, + sandbox, + useCSPSandbox, + triggerMethod + ); + await errorPromise; + + ok( + errorPromise, + "Should not show the dialog for iframe with sandbox " + sandbox + ); + } else { + let dialogWindowOpenPromise = waitForProtocolAppChooserDialog( + browser, + true + ); + await navigateExternalProtoFromIframe( + browser, + sandbox, + useCSPSandbox, + triggerMethod + ); + let dialog = await dialogWindowOpenPromise; + + ok(dialog, "Should show the dialog for sandbox " + sandbox); + + // Close dialog before closing the tab to avoid intermittent failures. + let dialogWindowClosePromise = waitForProtocolAppChooserDialog( + browser, + false + ); + + dialog.close(); + await dialogWindowClosePromise; + } + } + ); +} diff --git a/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs new file mode 100644 index 0000000000..8afa04cfe0 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + + if (!request.queryString.match(/^name=/)) { + return; + } + var name = decodeURIComponent(request.queryString.substring(5)); + + response.setHeader("Content-Type", 'image/png; name="' + name + '"'); + response.setHeader( + "Content-Disposition", + 'attachment; filename="' + name + '"' + ); +} diff --git a/uriloader/exthandler/tests/mochitest/mailto.html b/uriloader/exthandler/tests/mochitest/mailto.html new file mode 100644 index 0000000000..d507697443 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/mailto.html @@ -0,0 +1,11 @@ + + + + Mailto handler + + + + + mailto link + + diff --git a/uriloader/exthandler/tests/mochitest/mime_type_download.sjs b/uriloader/exthandler/tests/mochitest/mime_type_download.sjs new file mode 100644 index 0000000000..a33331d0cf --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/mime_type_download.sjs @@ -0,0 +1,21 @@ +function handleRequest(request, response) { + "use strict"; + Cu.importGlobalProperties(["URLSearchParams"]); + let content = ""; + let params = new URLSearchParams(request.queryString); + let extension = params.get("extension"); + let contentType = params.get("contentType"); + if (params.has("withHeader")) { + response.setHeader( + "Content-Disposition", + `attachment; filename="mime_type_download${ + extension ? "." + extension : "" + }";`, + false + ); + } + response.setHeader("Content-Type", contentType, false); + response.setHeader("Content-Length", "" + content.length, false); + response.setStatusLine(request.httpVersion, 200); + response.write(content); +} diff --git a/uriloader/exthandler/tests/mochitest/mochitest.ini b/uriloader/exthandler/tests/mochitest/mochitest.ini new file mode 100644 index 0000000000..c2ab1f5099 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/mochitest.ini @@ -0,0 +1,17 @@ +[test_invalidCharFileExtension.xhtml] +skip-if = toolkit == 'android' # Bug 1525959 +support-files = + HelperAppLauncherDialog_chromeScript.js + invalidCharFileExtension.sjs +[test_nullCharFile.xhtml] +skip-if = toolkit == 'android' # Bug 1525959 +support-files = + HelperAppLauncherDialog_chromeScript.js +[test_unknown_ext_protocol_handlers.html] +[test_unsafeBidiChars.xhtml] +skip-if = + toolkit == 'android' # Bug 1525959 + http3 +support-files = + HelperAppLauncherDialog_chromeScript.js + unsafeBidiFileName.sjs diff --git a/uriloader/exthandler/tests/mochitest/protocolHandler.html b/uriloader/exthandler/tests/mochitest/protocolHandler.html new file mode 100644 index 0000000000..eff8a53aab --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/protocolHandler.html @@ -0,0 +1,16 @@ + + + + Protocol handler + + + + + + testprotocol link + + diff --git a/uriloader/exthandler/tests/mochitest/protocol_custom_sandbox_helper.sjs b/uriloader/exthandler/tests/mochitest/protocol_custom_sandbox_helper.sjs new file mode 100644 index 0000000000..faf2937a08 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/protocol_custom_sandbox_helper.sjs @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + Cu.importGlobalProperties(["URLSearchParams"]); + + let query = new URLSearchParams(request.queryString); + + // Set CSP sandbox attributes if caller requests any. + let cspSandbox = query.get("cspSandbox"); + if (cspSandbox) { + response.setHeader( + "Content-Security-Policy", + "sandbox " + cspSandbox, + false + ); + } + + // Redirect to custom protocol via HTTP 302. + if (query.get("redirectCustomProtocol")) { + response.setStatusLine(request.httpVersion, 302, "Found"); + + response.setHeader("Location", "mailto:test@example.com", false); + response.write("Redirect!"); + return; + } + + response.setStatusLine(request.httpVersion, 200); + response.write("OK"); +} diff --git a/uriloader/exthandler/tests/mochitest/redirect_helper.sjs b/uriloader/exthandler/tests/mochitest/redirect_helper.sjs new file mode 100644 index 0000000000..5c1068bebb --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/redirect_helper.sjs @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Cu.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + let params = new URLSearchParams(request.queryString); + let uri = params.get("uri"); + let redirectType = params.get("redirectType") || "location"; + switch (redirectType) { + case "refresh": + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Refresh", "0; url=" + uri); + break; + + case "meta-refresh": + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html"); + response.write(``); + break; + + case "location": + // fall through + default: + response.setStatusLine(request.httpVersion, 302, "Moved Temporarily"); + response.setHeader("Location", uri); + } +} diff --git a/uriloader/exthandler/tests/mochitest/save_filenames.html b/uriloader/exthandler/tests/mochitest/save_filenames.html new file mode 100644 index 0000000000..1535a0f657 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/save_filenames.html @@ -0,0 +1,360 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Link + Link + Link + Link + + Link + Link + Link> + Link + Link + + + + One + Two + Three + Four + Five + + + + + + diff --git a/uriloader/exthandler/tests/mochitest/script_redirect.html b/uriloader/exthandler/tests/mochitest/script_redirect.html new file mode 100644 index 0000000000..31e0dc6a7e --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/script_redirect.html @@ -0,0 +1,5 @@ + diff --git a/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml new file mode 100644 index 0000000000..177af3757f --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml @@ -0,0 +1,65 @@ + + + Test for Handling of unsafe bidi chars + + + + +

+ + + + diff --git a/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml new file mode 100644 index 0000000000..b153395e81 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml @@ -0,0 +1,67 @@ + + + Test for Handling of null char + + + + +

+ + + + diff --git a/uriloader/exthandler/tests/mochitest/test_spammy_page.html b/uriloader/exthandler/tests/mochitest/test_spammy_page.html new file mode 100644 index 0000000000..b1e60a1e8e --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_spammy_page.html @@ -0,0 +1,27 @@ + + + + + + ... + + +

Hello, it's the spammy page!

+ +Image + + diff --git a/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html b/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html new file mode 100644 index 0000000000..f8727db605 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html @@ -0,0 +1,28 @@ + + + + Test for no error reporting for unknown external protocols + + + + + + + + diff --git a/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml new file mode 100644 index 0000000000..34c6c956fd --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml @@ -0,0 +1,89 @@ + + + Test for Handling of unsafe bidi chars + + + + +

+ + + + diff --git a/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs new file mode 100644 index 0000000000..4f88ff6de2 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + + if (!request.queryString.match(/^name=/)) { + return; + } + var name = decodeURIComponent(request.queryString.substring(5)); + + response.setHeader( + "Content-Type", + 'application/octet-stream; name="' + name + '"' + ); + response.setHeader("Content-Disposition", 'inline; filename="' + name + '"'); +} diff --git a/uriloader/exthandler/tests/moz.build b/uriloader/exthandler/tests/moz.build new file mode 100644 index 0000000000..691ab2932a --- /dev/null +++ b/uriloader/exthandler/tests/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/. + +MOCHITEST_MANIFESTS += ["mochitest/mochitest.ini"] + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"] + +BROWSER_CHROME_MANIFESTS += ["mochitest/browser.ini"] + +TEST_DIRS += [ + "gtest", +] + +TESTING_JS_MODULES += [ + "HandlerServiceTestUtils.sys.mjs", +] + +GeckoSimplePrograms( + [ + "WriteArgument", + ], + linkage=None, +) + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.xpcshell.uriloader.exthandler.tests.unit += [ + "!WriteArgument%s" % CONFIG["BIN_SUFFIX"] + ] + +USE_LIBS += [ + "nspr", +] diff --git a/uriloader/exthandler/tests/unit/handlers.json b/uriloader/exthandler/tests/unit/handlers.json new file mode 100644 index 0000000000..51ce581d83 --- /dev/null +++ b/uriloader/exthandler/tests/unit/handlers.json @@ -0,0 +1,83 @@ +{ + "defaultHandlersVersion": { + "en-US": 999 + }, + "mimeTypes": { + "example/type.handleinternally": { + "unknownProperty": "preserved", + "action": 3, + "extensions": ["example_one"] + }, + "example/type.savetodisk": { + "action": 0, + "ask": true, + "handlers": [ + { + "name": "Example Default Handler", + "uriTemplate": "https://www.example.com/?url=%s" + } + ], + "extensions": ["example_two", "example_three"] + }, + "example/type.usehelperapp": { + "action": 2, + "ask": true, + "handlers": [ + { + "name": "Example Default Handler", + "uriTemplate": "https://www.example.com/?url=%s" + }, + { + "name": "Example Possible Handler One", + "uriTemplate": "http://www.example.com/?id=1&url=%s" + }, + { + "name": "Example Possible Handler Two", + "uriTemplate": "http://www.example.com/?id=2&url=%s" + } + ], + "extensions": ["example_two", "example_three"] + }, + "example/type.usesystemdefault": { + "action": 4, + "handlers": [ + null, + { + "name": "Example Possible Handler", + "uriTemplate": "http://www.example.com/?url=%s" + } + ] + } + }, + "schemes": { + "examplescheme.usehelperapp": { + "action": 2, + "ask": true, + "handlers": [ + { + "name": "Example Default Handler", + "uriTemplate": "https://www.example.com/?url=%s" + }, + { + "name": "Example Possible Handler One", + "uriTemplate": "http://www.example.com/?id=1&url=%s" + }, + { + "name": "Example Possible Handler Two", + "uriTemplate": "http://www.example.com/?id=2&url=%s" + } + ] + }, + "examplescheme.usesystemdefault": { + "action": 4, + "handlers": [ + null, + { + "name": "Example Possible Handler", + "uriTemplate": "http://www.example.com/?url=%s" + } + ] + } + }, + "isDownloadsImprovementsAlreadyMigrated": true +} diff --git a/uriloader/exthandler/tests/unit/head.js b/uriloader/exthandler/tests/unit/head.js new file mode 100644 index 0000000000..2f65582ae9 --- /dev/null +++ b/uriloader/exthandler/tests/unit/head.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/. */ + +/* + * Initialization for tests related to invoking external handler applications. + */ + +"use strict"; + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const { HandlerServiceTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/HandlerServiceTestUtils.sys.mjs" +); +var { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gHandlerService", + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService" +); + +do_get_profile(); + +let jsonPath = PathUtils.join(PathUtils.profileDir, "handlers.json"); + +/** + * Unloads the nsIHandlerService data store, so the back-end file can be + * accessed or modified, and the new data will be loaded at the next access. + */ +let unloadHandlerStore = async function () { + // If this function is called before the nsIHandlerService instance has been + // initialized for the first time, the observer below will not be registered. + // We have to force initialization to prevent the function from stalling. + gHandlerService; + + let promise = TestUtils.topicObserved("handlersvc-json-replace-complete"); + Services.obs.notifyObservers(null, "handlersvc-json-replace"); + await promise; +}; + +/** + * Unloads the data store and deletes it. + */ +let deleteHandlerStore = async function () { + await unloadHandlerStore(); + + await IOUtils.remove(jsonPath, { ignoreAbsent: true }); + + Services.prefs.clearUserPref("gecko.handlerService.defaultHandlersVersion"); +}; + +/** + * Unloads the data store and replaces it with the test data file. + */ +let copyTestDataToHandlerStore = async function () { + await unloadHandlerStore(); + + await IOUtils.copy(do_get_file("handlers.json").path, jsonPath); + + Services.prefs.setIntPref("gecko.handlerService.defaultHandlersVersion", 100); +}; + +/** + * Ensures the files are removed and the services unloaded when the tests end. + */ +registerCleanupFunction(async function test_terminate() { + await deleteHandlerStore(); +}); diff --git a/uriloader/exthandler/tests/unit/mailcap b/uriloader/exthandler/tests/unit/mailcap new file mode 100644 index 0000000000..dc93ef8042 --- /dev/null +++ b/uriloader/exthandler/tests/unit/mailcap @@ -0,0 +1,2 @@ +text/plain; cat '%s'; needsterminal +text/plain; sed '%s' diff --git a/uriloader/exthandler/tests/unit/test_badMIMEType.js b/uriloader/exthandler/tests/unit/test_badMIMEType.js new file mode 100644 index 0000000000..49c5e8d848 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_badMIMEType.js @@ -0,0 +1,29 @@ +/* -*- 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 run_test() { + // "text/plain" has an 0xFF character appended to it. This means it's an + // invalid string, which is tricky to enter using a text editor (I used + // emacs' hexl-mode). It also means an ordinary text editor might drop it + // or convert it to something that *is* valid (in UTF8). So we measure + // its length to make sure this hasn't happened. + var badMimeType = "text/plainÿ"; + Assert.equal(badMimeType.length, 11); + try { + Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getFromTypeAndExtension(badMimeType, "txt"); + } catch (e) { + if ( + !(e instanceof Ci.nsIException) || + e.result != Cr.NS_ERROR_NOT_AVAILABLE + ) { + throw e; + } + // This is an expected exception, thrown if the type can't be determined + } + // Not crashing is good enough + Assert.equal(true, true); +} diff --git a/uriloader/exthandler/tests/unit/test_defaults_handlerService.js b/uriloader/exthandler/tests/unit/test_defaults_handlerService.js new file mode 100644 index 0000000000..47fd18a642 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_defaults_handlerService.js @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "gExternalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); +ChromeUtils.defineESModuleGetters(this, { + kHandlerList: "resource://gre/modules/handlers/HandlerList.sys.mjs", +}); + +add_task(async function test_check_defaults_get_added() { + let protocols = Object.keys(kHandlerList.default.schemes); + for (let protocol of protocols) { + let protocolHandlerCount = + kHandlerList.default.schemes[protocol].handlers.length; + Assert.ok( + gHandlerService.wrappedJSObject._store.data.schemes[protocol].stubEntry, + `Expect stub for ${protocol}` + ); + let info = gExternalProtocolService.getProtocolHandlerInfo(protocol, {}); + Assert.ok( + info, + `Should be able to get protocol handler info for ${protocol}` + ); + let handlers = Array.from( + info.possibleApplicationHandlers.enumerate(Ci.nsIHandlerApp) + ); + handlers = handlers.filter(h => h instanceof Ci.nsIWebHandlerApp); + Assert.equal( + handlers.length, + protocolHandlerCount, + `Default web handlers for ${protocol} should match` + ); + let { alwaysAskBeforeHandling, preferredAction } = info; + // Actually store something, pretending there was a change: + let infoToWrite = gExternalProtocolService.getProtocolHandlerInfo( + protocol, + {} + ); + gHandlerService.store(infoToWrite); + ok( + !gHandlerService.wrappedJSObject._store.data.schemes[protocol].stubEntry, + "Expect stub entry info to go away" + ); + + let newInfo = gExternalProtocolService.getProtocolHandlerInfo(protocol, {}); + Assert.equal( + alwaysAskBeforeHandling, + newInfo.alwaysAskBeforeHandling, + protocol + " - always ask shouldn't change" + ); + Assert.equal( + preferredAction, + newInfo.preferredAction, + protocol + " - preferred action shouldn't change" + ); + await deleteHandlerStore(); + } +}); + +add_task(async function test_check_default_modification() { + Assert.ok( + true, + JSON.stringify(gHandlerService.wrappedJSObject._store.data.schemes.mailto) + ); + Assert.ok( + gHandlerService.wrappedJSObject._store.data.schemes.mailto.stubEntry, + "Expect stub for mailto" + ); + let mailInfo = gExternalProtocolService.getProtocolHandlerInfo("mailto", {}); + mailInfo.alwaysAskBeforeHandling = false; + mailInfo.preferredAction = Ci.nsIHandlerInfo.useSystemDefault; + gHandlerService.store(mailInfo); + Assert.ok( + !gHandlerService.wrappedJSObject._store.data.schemes.mailto.stubEntry, + "Stub entry should be removed immediately." + ); + let newMail = gExternalProtocolService.getProtocolHandlerInfo("mailto", {}); + Assert.equal(newMail.preferredAction, Ci.nsIHandlerInfo.useSystemDefault); + Assert.equal(newMail.alwaysAskBeforeHandling, false); + await deleteHandlerStore(); +}); + +add_task(async function test_migrations() { + const kTestData = [ + ["A", "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s"], + ["B", "http://www.inbox.lv/rfc2368/?value=%s"], + ["C", "http://poczta.interia.pl/mh/?mailto=%s"], + ["D", "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s"], + ]; + // Set up the test handlers. This doesn't use prefs like the previous test, + // because we now refuse to import insecure handler prefs. They can only + // exist if they were added into the handler store before this restriction + // was created (bug 1526890). + gHandlerService.wrappedJSObject._injectDefaultProtocolHandlers(); + let handler = gExternalProtocolService.getProtocolHandlerInfo("mailto"); + while (handler.possibleApplicationHandlers.length) { + handler.possibleApplicationHandlers.removeElementAt(0); + } + for (let [name, uriTemplate] of kTestData) { + let app = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + app.uriTemplate = uriTemplate; + app.name = name; + handler.possibleApplicationHandlers.appendElement(app); + } + gHandlerService.store(handler); + + // Now migrate them: + Services.prefs.setCharPref("browser.handlers.migrations", "blah,secure-mail"); + gHandlerService.wrappedJSObject._migrateProtocolHandlersIfNeeded(); + + // Now check the result: + handler = gExternalProtocolService.getProtocolHandlerInfo("mailto"); + + let expectedURIs = new Set([ + "https://mail.yahoo.co.jp/compose/?To=%s", + "https://mail.inbox.lv/compose?to=%s", + "https://poczta.interia.pl/mh/?mailto=%s", + "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s", + ]); + + let possibleApplicationHandlers = Array.from( + handler.possibleApplicationHandlers.enumerate(Ci.nsIWebHandlerApp) + ); + // Set iterators are stable to deletion, so this works: + for (let expected of expectedURIs) { + for (let app of possibleApplicationHandlers) { + if (app instanceof Ci.nsIWebHandlerApp && app.uriTemplate == expected) { + Assert.ok(true, "Found handler with URI " + expected); + // ... even when we remove items. + expectedURIs.delete(expected); + break; + } + } + } + Assert.equal(expectedURIs.size, 0, "Should have seen all the expected URIs."); + + for (let app of possibleApplicationHandlers) { + if (app instanceof Ci.nsIWebHandlerApp) { + Assert.ok( + !kTestData.some(n => n[1] == app.uriTemplate), + "Should not be any of the original handlers" + ); + } + } + + Assert.ok( + !handler.preferredApplicationHandler, + "Shouldn't have preferred handler initially." + ); + await deleteHandlerStore(); +}); diff --git a/uriloader/exthandler/tests/unit/test_downloads_improvements_migration.js b/uriloader/exthandler/tests/unit/test_downloads_improvements_migration.js new file mode 100644 index 0000000000..49e4493b23 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_downloads_improvements_migration.js @@ -0,0 +1,244 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + +const { Integration } = ChromeUtils.importESModule( + "resource://gre/modules/Integration.sys.mjs" +); + +/* global DownloadIntegration */ +Integration.downloads.defineESModuleGetter( + this, + "DownloadIntegration", + "resource://gre/modules/DownloadIntegration.sys.mjs" +); + +/** + * Tests that the migration runs and that only + * files with preferredAction alwaysAsk are updated. + */ +add_task(async function test_migration() { + // Create mock implementation of shouldDownloadInternally for test case + let oldShouldViewDownloadInternally = + DownloadIntegration.shouldViewDownloadInternally; + DownloadIntegration.shouldViewDownloadInternally = (mimeType, extension) => { + let downloadTypesViewableInternally = [ + { + extension: "pdf", + mimeTypes: ["application/pdf"], + }, + { + extension: "webp", + mimeTypes: ["image/webp"], + }, + ]; + + for (const mockHandler of downloadTypesViewableInternally) { + if (mockHandler.mimeTypes.includes(mimeType)) { + return true; + } + } + + return false; + }; + + registerCleanupFunction(async function () { + Services.prefs.clearUserPref( + "browser.download.improvements_to_download_panel" + ); + DownloadIntegration.shouldViewDownloadInternally = + oldShouldViewDownloadInternally; + }); + + // For setup, set pref to false. Will be enabled later. + Services.prefs.setBoolPref( + "browser.download.improvements_to_download_panel", + false + ); + + // Plain text file + let txtHandlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", "txt"); + txtHandlerInfo.preferredAction = Ci.nsIHandlerInfo.alwaysAsk; + txtHandlerInfo.alwaysAskBeforeHandling = true; + // PDF file + let pdfHandlerInfo = mimeSvc.getFromTypeAndExtension( + "application/pdf", + "pdf" + ); + pdfHandlerInfo.preferredAction = Ci.nsIHandlerInfo.alwaysAsk; + pdfHandlerInfo.alwaysAskBeforeHandling = true; + // WebP file + let webpHandlerInfo = mimeSvc.getFromTypeAndExtension("image/webp", "webp"); + webpHandlerInfo.preferredAction = Ci.nsIHandlerInfo.useSystemDefault; + webpHandlerInfo.alwaysAskBeforeHandling = false; + + handlerSvc.store(txtHandlerInfo); + handlerSvc.store(pdfHandlerInfo); + handlerSvc.store(webpHandlerInfo); + + Services.prefs.setBoolPref( + "browser.download.improvements_to_download_panel", + true + ); + gHandlerService.wrappedJSObject._migrateDownloadsImprovementsIfNeeded(); + + txtHandlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", "txt"); + pdfHandlerInfo = mimeSvc.getFromTypeAndExtension("application/pdf", "pdf"); + webpHandlerInfo = mimeSvc.getFromTypeAndExtension("image/webp", "webp"); + let data = gHandlerService.wrappedJSObject._store.data; + Assert.equal( + data.isDownloadsImprovementsAlreadyMigrated, + true, + "isDownloadsImprovementsAlreadyMigrated should be set to true" + ); + Assert.equal( + pdfHandlerInfo.preferredAction, + Ci.nsIHandlerInfo.handleInternally, + "application/pdf - preferredAction should be handleInternally" + ); + Assert.equal( + pdfHandlerInfo.alwaysAskBeforeHandling, + false, + "application/pdf - alwaysAskBeforeHandling should be false" + ); + Assert.equal( + webpHandlerInfo.preferredAction, + Ci.nsIHandlerInfo.useSystemDefault, + "image/webp - preferredAction should be useSystemDefault" + ); + Assert.equal( + webpHandlerInfo.alwaysAskBeforeHandling, + false, + "image/webp - alwaysAskBeforeHandling should be false" + ); + Assert.equal( + txtHandlerInfo.preferredAction, + Ci.nsIHandlerInfo.saveToDisk, + "text/plain - preferredAction should be saveToDisk" + ); + Assert.equal( + txtHandlerInfo.alwaysAskBeforeHandling, + false, + "text/plain - alwaysAskBeforeHandling should be false" + ); +}); + +/** + * Tests that the migration does not run if the migration was already run. + */ +add_task(async function test_migration_already_run() { + let data = gHandlerService.wrappedJSObject._store.data; + data.isDownloadsImprovementsAlreadyMigrated = true; + + // Plain text file + let txtHandlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", "txt"); + txtHandlerInfo.preferredAction = Ci.nsIHandlerInfo.alwaysAsk; + txtHandlerInfo.alwaysAskBeforeHandling = true; + // PDF file + let pdfHandlerInfo = mimeSvc.getFromTypeAndExtension( + "application/pdf", + "pdf" + ); + pdfHandlerInfo.preferredAction = Ci.nsIHandlerInfo.alwaysAsk; + pdfHandlerInfo.alwaysAskBeforeHandling = true; + + handlerSvc.store(txtHandlerInfo); + handlerSvc.store(pdfHandlerInfo); + + gHandlerService.wrappedJSObject._migrateDownloadsImprovementsIfNeeded(); + + txtHandlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", "txt"); + pdfHandlerInfo = mimeSvc.getFromTypeAndExtension("application/pdf", "pdf"); + data = gHandlerService.wrappedJSObject._store.data; + Assert.equal( + pdfHandlerInfo.preferredAction, + Ci.nsIHandlerInfo.alwaysAsk, + "application/pdf - preferredAction should be alwaysAsk" + ); + Assert.equal( + pdfHandlerInfo.alwaysAskBeforeHandling, + true, + "application/pdf - alwaysAskBeforeHandling should be true" + ); + Assert.equal( + txtHandlerInfo.preferredAction, + Ci.nsIHandlerInfo.alwaysAsk, + "text/plain - preferredAction should be alwaysAsk" + ); + Assert.equal( + txtHandlerInfo.alwaysAskBeforeHandling, + true, + "text/plain - alwaysAskBeforeHandling should be true" + ); +}); + +/** + * Test migration of SVG and XML info. + */ +add_task(async function test_migration_xml_svg() { + let data = gHandlerService.wrappedJSObject._store.data; + // Plain text file + let txtHandlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", "txt"); + txtHandlerInfo.preferredAction = Ci.nsIHandlerInfo.alwaysAsk; + txtHandlerInfo.alwaysAskBeforeHandling = true; + // SVG file + let svgHandlerInfo = mimeSvc.getFromTypeAndExtension("image/svg+xml", "svg"); + svgHandlerInfo.preferredAction = Ci.nsIHandlerInfo.handleInternally; + svgHandlerInfo.alwaysAskBeforeHandling = false; + // XML file + let xmlHandlerInfo = mimeSvc.getFromTypeAndExtension("text/xml", "xml"); + xmlHandlerInfo.preferredAction = Ci.nsIHandlerInfo.handleInternally; + xmlHandlerInfo.alwaysAskBeforeHandling = false; + + handlerSvc.store(txtHandlerInfo); + handlerSvc.store(svgHandlerInfo); + handlerSvc.store(xmlHandlerInfo); + + gHandlerService.wrappedJSObject._migrateSVGXMLIfNeeded(); + + txtHandlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", "txt"); + svgHandlerInfo = mimeSvc.getFromTypeAndExtension("image/svg+xml", "svg"); + xmlHandlerInfo = mimeSvc.getFromTypeAndExtension("text/xml", "xml"); + data = gHandlerService.wrappedJSObject._store.data; + Assert.equal( + svgHandlerInfo.preferredAction, + Ci.nsIHandlerInfo.saveToDisk, + "image/svg+xml - preferredAction should be saveToDisk" + ); + Assert.equal( + svgHandlerInfo.alwaysAskBeforeHandling, + false, + "image/svg+xml - alwaysAskBeforeHandling should be false" + ); + Assert.equal( + xmlHandlerInfo.preferredAction, + Ci.nsIHandlerInfo.saveToDisk, + "text/xml - preferredAction should be saveToDisk" + ); + Assert.equal( + xmlHandlerInfo.alwaysAskBeforeHandling, + false, + "text/xml - alwaysAskBeforeHandling should be false" + ); + Assert.equal( + txtHandlerInfo.preferredAction, + Ci.nsIHandlerInfo.alwaysAsk, + "text/plain - preferredAction should be alwaysAsk" + ); + Assert.equal( + txtHandlerInfo.alwaysAskBeforeHandling, + true, + "text/plain - alwaysAskBeforeHandling should be true" + ); + + ok( + data.isSVGXMLAlreadyMigrated, + "Should have stored migration state on the data object." + ); +}); diff --git a/uriloader/exthandler/tests/unit/test_filename_sanitize.js b/uriloader/exthandler/tests/unit/test_filename_sanitize.js new file mode 100644 index 0000000000..d8ab12c266 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_filename_sanitize.js @@ -0,0 +1,398 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test verifies that +// nsIMIMEService.validateFileNameForSaving sanitizes filenames +// properly with different flags. + +"use strict"; + +add_task(async function validate_filename_method() { + let mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + + function checkFilename(filename, flags, mime = "image/png") { + return mimeService.validateFileNameForSaving(filename, mime, flags); + } + + Assert.equal(checkFilename("basicfile.png", 0), "basicfile.png"); + Assert.equal(checkFilename(" whitespace.png ", 0), "whitespace.png"); + Assert.equal( + checkFilename(" .whitespaceanddots.png...", 0), + "whitespaceanddots.png" + ); + Assert.equal( + checkFilename(" \u00a0 \u00a0 extrawhitespace.png \u00a0 \u00a0 ", 0), + "extrawhitespace.png" + ); + Assert.equal( + checkFilename(" filename with whitespace.png ", 0), + "filename with whitespace.png" + ); + Assert.equal(checkFilename("\\path.png", 0), "_path.png"); + Assert.equal( + checkFilename("\\path*and/$?~file.png", 0), + "_path and_$ ~file.png" + ); + Assert.equal( + checkFilename(" \u180e whit\u180ee.png \u180e", 0), + "whit\u180ee.png" + ); + Assert.equal(checkFilename("簡単簡単簡単", 0), "簡単簡単簡単.png"); + Assert.equal(checkFilename(" happy\u061c\u2069.png", 0), "happy__.png"); + Assert.equal( + checkFilename("12345678".repeat(31) + "abcdefgh.png", 0), + "12345678".repeat(31) + "ab.png" + ); + Assert.equal( + checkFilename("簡単".repeat(41) + ".png", 0), + "簡単".repeat(41) + ".png" + ); + Assert.equal( + checkFilename("a" + "簡単".repeat(42) + ".png", 0), + "a" + "簡単".repeat(40) + "簡.png" + ); + Assert.equal( + checkFilename("a" + "簡単".repeat(56) + ".png", 0), + "a" + "簡単".repeat(40) + ".png" + ); + Assert.equal(checkFilename("café.png", 0), "café.png"); + Assert.equal( + checkFilename("café".repeat(50) + ".png", 0), + "café".repeat(50) + ".png" + ); + Assert.equal( + checkFilename("café".repeat(51) + ".png", 0), + "café".repeat(49) + "caf.png" + ); + + Assert.equal( + checkFilename("\u{100001}\u{100002}.png", 0), + "\u{100001}\u{100002}.png" + ); + Assert.equal( + checkFilename("\u{100001}\u{100002}".repeat(31) + ".png", 0), + "\u{100001}\u{100002}".repeat(31) + ".png" + ); + Assert.equal( + checkFilename("\u{100001}\u{100002}".repeat(32) + ".png", 0), + "\u{100001}\u{100002}".repeat(30) + "\u{100001}.png" + ); + + Assert.equal( + checkFilename("noextensionfile".repeat(16), 0), + "noextensionfile".repeat(16) + ".png" + ); + Assert.equal( + checkFilename("noextensionfile".repeat(17), 0), + "noextensionfile".repeat(16) + "noextensio.png" + ); + Assert.equal( + checkFilename("noextensionfile".repeat(16) + "noextensionfil.", 0), + "noextensionfile".repeat(16) + "noextensio.png" + ); + + Assert.equal(checkFilename(" first .png ", 0), "first .png"); + Assert.equal( + checkFilename( + " second .png ", + mimeService.VALIDATE_DONT_COLLAPSE_WHITESPACE + ), + "second .png" + ); + + // For whatever reason, the Android mime handler accepts the .jpeg + // extension for image/png, so skip this test there. + if (AppConstants.platform != "android") { + Assert.equal(checkFilename("thi/*rd.jpeg", 0), "thi_ rd.png"); + } + + Assert.equal( + checkFilename("f*\\ourth file.jpg", mimeService.VALIDATE_SANITIZE_ONLY), + "f _ourth file.jpg" + ); + Assert.equal( + checkFilename( + "f*\\ift h.jpe*\\g", + mimeService.VALIDATE_SANITIZE_ONLY | + mimeService.VALIDATE_DONT_COLLAPSE_WHITESPACE + ), + "f _ift h.jpe _g" + ); + Assert.equal(checkFilename("sixth.j pe/*g", 0), "sixth.png"); + + let repeatStr = "12345678".repeat(31); + Assert.equal( + checkFilename( + repeatStr + "seventh.png", + mimeService.VALIDATE_DONT_TRUNCATE + ), + repeatStr + "seventh.png" + ); + Assert.equal( + checkFilename(repeatStr + "seventh.png", 0), + repeatStr + "se.png" + ); + + // no filename, so index is used by default. + Assert.equal(checkFilename(".png", 0), "png.png"); + + // sanitization only, so Untitled is not added, but initial period is stripped. + Assert.equal( + checkFilename(".png", mimeService.VALIDATE_SANITIZE_ONLY), + "png" + ); + + // correct .png extension is applied. + Assert.equal(checkFilename(".butterpecan.icecream", 0), "butterpecan.png"); + + // sanitization only, so extension is not modified, but initial period is stripped. + Assert.equal( + checkFilename(".butterpecan.icecream", mimeService.VALIDATE_SANITIZE_ONLY), + "butterpecan.icecream" + ); + + let ext = ".fairlyLongExtension"; + Assert.equal( + checkFilename(repeatStr + ext, mimeService.VALIDATE_SANITIZE_ONLY), + repeatStr.substring(0, 254 - ext.length) + ext + ); + + ext = "lo%?n/ginvalid? ch\\ars"; + Assert.equal( + checkFilename(repeatStr + ext, mimeService.VALIDATE_SANITIZE_ONLY), + repeatStr + "lo% n_" + ); + + ext = ".long/invalid%? ch\\ars"; + Assert.equal( + checkFilename(repeatStr + ext, mimeService.VALIDATE_SANITIZE_ONLY), + repeatStr.substring(0, 233) + ".long_invalid% ch_ars" + ); + + Assert.equal( + checkFilename("test_テスト_T\x83E\\S\x83T.png", 0), + "test_テスト_T E_S T.png" + ); + Assert.equal( + checkFilename("test_テスト_T\x83E\\S\x83T.pテ\x83ng", 0), + "test_テスト_T E_S T.png" + ); + + // Check we don't invalidate surrogate pairs when trimming. + Assert.equal(checkFilename("test😀", 0, ""), "test😀"); + Assert.equal(checkFilename("test😀😀", 0, ""), "test😀😀"); + + // Now check some media types + Assert.equal( + mimeService.validateFileNameForSaving("video.ogg", "video/ogg", 0), + "video.ogg", + "video.ogg" + ); + Assert.equal( + mimeService.validateFileNameForSaving("video.ogv", "video/ogg", 0), + "video.ogv", + "video.ogv" + ); + Assert.equal( + mimeService.validateFileNameForSaving("video.ogt", "video/ogg", 0), + "video.ogv", + "video.ogt" + ); + + Assert.equal( + mimeService.validateFileNameForSaving("audio.mp3", "audio/mpeg", 0), + "audio.mp3", + "audio.mp3" + ); + Assert.equal( + mimeService.validateFileNameForSaving("audio.mpega", "audio/mpeg", 0), + "audio.mpega", + "audio.mpega" + ); + Assert.equal( + mimeService.validateFileNameForSaving("audio.mp2", "audio/mpeg", 0), + "audio.mp2", + "audio.mp2" + ); + + let expected = "audio.mp3"; + if (AppConstants.platform == "linux") { + expected = "audio.mpga"; + } else if (AppConstants.platform == "android") { + expected = "audio.mp4"; + } + + Assert.equal( + mimeService.validateFileNameForSaving("audio.mp4", "audio/mpeg", 0), + expected, + "audio.mp4" + ); + + Assert.equal( + mimeService.validateFileNameForSaving("sound.m4a", "audio/mp4", 0), + "sound.m4a", + "sound.m4a" + ); + Assert.equal( + mimeService.validateFileNameForSaving("sound.m4b", "audio/mp4", 0), + AppConstants.platform == "android" ? "sound.m4a" : "sound.m4b", + "sound.m4b" + ); + Assert.equal( + mimeService.validateFileNameForSaving("sound.m4c", "audio/mp4", 0), + AppConstants.platform == "macosx" ? "sound.mp4" : "sound.m4a", + "sound.mpc" + ); + + // This has a long filename with a 13 character extension. The end of the filename should be + // cropped to fit into 255 bytes. + Assert.equal( + mimeService.validateFileNameForSaving( + "라이브9.9만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24%102 000원 브랜드데이 앵콜 🎁 1.등 유산균 컬처렐 특가!", + "text/unknown", + mimeService.VALIDATE_SANITIZE_ONLY + ), + "라이브9.9만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 .등 유산균 컬처렐 특가!", + "very long filename with extension" + ); + + // This filename has a very long extension, almost the entire filename. + Assert.equal( + mimeService.validateFileNameForSaving( + "라이브9.9만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24%102 000원 브랜드데이 앵콜 🎁 1등 유산균 컬처렐 특가!", + "text/unknown", + mimeService.VALIDATE_SANITIZE_ONLY + ), + "라이브9", + "another very long filename with long extension" + ); + + // This filename is cropped at 254 bytes. + Assert.equal( + mimeService.validateFileNameForSaving( + ".라이브99만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24%102 000원 브랜드데이 앵콜 🎁 1등 유산균 컬처렐 특가!", + "text/unknown", + mimeService.VALIDATE_SANITIZE_ONLY + ), + "라이브99만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24%102 000원 브랜드데", + "very filename with extension only" + ); + + Assert.equal( + mimeService.validateFileNameForSaving("filename.LNK", "text/unknown", 0), + "filename.LNK.download", + "filename.LNK" + ); + Assert.equal( + mimeService.validateFileNameForSaving("filename.local", "text/unknown", 0), + "filename.local.download", + "filename.local" + ); + Assert.equal( + mimeService.validateFileNameForSaving("filename.url", "text/unknown", 0), + "filename.url.download", + "filename.url" + ); + Assert.equal( + mimeService.validateFileNameForSaving("filename.URl", "text/unknown", 0), + "filename.URl.download", + "filename.URl" + ); + Assert.equal( + mimeService.validateFileNameForSaving("filename.scf", "text/unknown", 0), + "filename.scf.download", + "filename.scf" + ); + Assert.equal( + mimeService.validateFileNameForSaving("filename.sCF", "text/unknown", 0), + "filename.sCF.download", + "filename.sCF" + ); + + Assert.equal( + mimeService.validateFileNameForSaving("filename.lnk\n", "text/unknown", 0), + "filename.lnk.download", + "filename.lnk with newline" + ); + + Assert.equal( + mimeService.validateFileNameForSaving( + "filename.lnk\n ", + "text/unknown", + 0 + ), + "filename.lnk.download", + "filename.lnk with newline" + ); + + Assert.equal( + mimeService.validateFileNameForSaving( + "filename.\n\t lnk", + "text/unknown", + 0 + ), + "filename. lnk", + "filename.lnk with space and newline" + ); + + Assert.equal( + mimeService.validateFileNameForSaving( + "filename.local\u180e\u180e\u180e", + "text/unknown", + 0 + ), + "filename.local.download", + "filename.lnk with vowel separators" + ); + + Assert.equal( + mimeService.validateFileNameForSaving( + "filename.LNK", + "text/unknown", + mimeService.VALIDATE_SANITIZE_ONLY + ), + "filename.LNK.download", + "filename.LNK sanitize only" + ); + + Assert.equal( + mimeService.validateFileNameForSaving( + "filename.LNK\n", + "text/unknown", + mimeService.VALIDATE_ALLOW_INVALID_FILENAMES + ), + "filename.LNK", + "filename.LNK allow invalid" + ); + + Assert.equal( + mimeService.validateFileNameForSaving( + "filename.URL\n", + "text/unknown", + mimeService.VALIDATE_SANITIZE_ONLY | + mimeService.VALIDATE_ALLOW_INVALID_FILENAMES + ), + "filename.URL", + "filename.URL allow invalid, sanitize only" + ); + + Assert.equal( + mimeService.validateFileNameForSaving( + "filename.desktop", + "text/unknown", + mimeService.VALIDATE_SANITIZE_ONLY + ), + "filename.desktop.download", + "filename.desktop sanitize only" + ); + Assert.equal( + mimeService.validateFileNameForSaving( + "filename.DESKTOP\n", + "text/unknown", + mimeService.VALIDATE_SANITIZE_ONLY | + mimeService.VALIDATE_ALLOW_INVALID_FILENAMES + ), + "filename.DESKTOP", + "filename.DESKTOP allow invalid, sanitize only" + ); +}); diff --git a/uriloader/exthandler/tests/unit/test_getFromTypeAndExtension.js b/uriloader/exthandler/tests/unit/test_getFromTypeAndExtension.js new file mode 100644 index 0000000000..6f4fe52a49 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getFromTypeAndExtension.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_utf8_extension() { + const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + let someMIME = mimeService.getFromTypeAndExtension( + "application/x-nonsense", + ".тест" + ); + Assert.stringContains(someMIME.description, "тест"); + // primary extension isn't set on macOS or android, see bug 1721181 + if (AppConstants.platform != "macosx" && AppConstants.platform != "android") { + Assert.equal(someMIME.primaryExtension, ".тест"); + } +}); diff --git a/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js b/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js new file mode 100644 index 0000000000..03b0cea25e --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "gMIMEService", + "@mozilla.org/mime;1", + "nsIMIMEService" +); + +// PDF files should always have a generic description instead +// of relying on what is registered with the Operating System. +add_task(async function test_check_unknown_mime_type() { + const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + let pdfType = mimeService.getTypeFromExtension("pdf"); + Assert.equal(pdfType, "application/pdf"); + let extension = mimeService.getPrimaryExtension("application/pdf", ""); + Assert.equal(extension, "pdf", "Expect pdf extension when given mime"); + let mimeInfo = gMIMEService.getFromTypeAndExtension("", "pdf"); + let stringBundle = Services.strings.createBundle( + "chrome://mozapps/locale/downloads/unknownContentType.properties" + ); + Assert.equal( + mimeInfo.description, + stringBundle.GetStringFromName("pdfExtHandlerDescription"), + "PDF has generic description" + ); +}); diff --git a/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js b/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js new file mode 100644 index 0000000000..9beef9d9c5 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Zip files can be opened by Windows explorer, so we should always be able to +// determine a description and default handler for them. However, things can +// get messy if they are sent to us with a mime type other than what Windows +// considers the "right" mimetype (application/x-zip-compressed), like +// application/zip, which is what most places (IANA, macOS, probably all linux +// distros, Apache, etc.) think is the "right" mimetype. +add_task(async function test_check_unknown_mime_type() { + const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + let zipType = mimeService.getTypeFromExtension("zip"); + Assert.equal(zipType, "application/x-zip-compressed"); + try { + let extension = mimeService.getPrimaryExtension("application/zip", ""); + Assert.equal( + extension, + "zip", + "Expect our own info to provide an extension for zip files." + ); + } catch (ex) { + Assert.ok(false, "We shouldn't throw when getting zip info."); + } + let found = {}; + let mimeInfo = mimeService.getMIMEInfoFromOS("application/zip", "zip", found); + Assert.ok( + mimeInfo.hasDefaultHandler, + "Should have a default app for zip files" + ); +}); diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js new file mode 100644 index 0000000000..7202db58de --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js @@ -0,0 +1,65 @@ +/* -*- 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 for bug 508030 : + * nsIMIMEService.getTypeFromExtension fails to find a match in the + * "ext-to-type-mapping" category if the provided extension is not lowercase. + */ +function run_test() { + // --- Common services --- + + const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + + const categoryManager = Services.catMan; + + // --- Test procedure --- + + const kTestExtension = "testextension"; + const kTestExtensionMixedCase = "testExtensIon"; + const kTestMimeType = "application/x-testextension"; + + // Ensure that the test extension is not initially recognized by the operating + // system or the "ext-to-type-mapping" category. + try { + // Try and get the MIME type associated with the extension. + mimeService.getTypeFromExtension(kTestExtension); + // The line above should have thrown an exception. + do_throw("nsIMIMEService.getTypeFromExtension succeeded unexpectedly"); + } catch (e) { + if ( + !(e instanceof Ci.nsIException) || + e.result != Cr.NS_ERROR_NOT_AVAILABLE + ) { + throw e; + } + // This is an expected exception, thrown if the type can't be determined. + // Any other exception would cause the test to fail. + } + + // Add a temporary category entry mapping the extension to the MIME type. + categoryManager.addCategoryEntry( + "ext-to-type-mapping", + kTestExtension, + kTestMimeType, + false, + true + ); + + // Check that the mapping is recognized in the simple case. + var type = mimeService.getTypeFromExtension(kTestExtension); + Assert.equal(type, kTestMimeType); + + // Check that the mapping is recognized even if the extension has mixed case. + type = mimeService.getTypeFromExtension(kTestExtensionMixedCase); + Assert.equal(type, kTestMimeType); + + // Clean up after ourselves. + categoryManager.deleteCategoryEntry( + "ext-to-type-mapping", + kTestExtension, + false + ); +} diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js new file mode 100644 index 0000000000..5076a57738 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js @@ -0,0 +1,218 @@ +/* -*- 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 for bug 484579 : + * nsIMIMEService.getTypeFromExtension may fail unexpectedly on Windows when + * "Content Type" is empty in the registry. + */ + +// We must use a file extension that isn't listed in nsExternalHelperAppService's +// defaultMimeEntries, otherwise the code takes a shortcut skipping the registry. +const FILE_EXTENSION = ".nfo"; +// This is used to ensure the test properly used the mock, so that if we change +// the underlying code, it won't be skipped. +let gTestUsedOurMock = false; + +function run_test() { + // Activate the override of the file association data in the registry. + registerMockWindowsRegKeyFactory(); + + // Check the mock has been properly activated. + let regKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + regKey.open( + Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT, + FILE_EXTENSION, + Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE + ); + Assert.equal( + regKey.readStringValue("content type"), + "", + "Check the mock replied as expected." + ); + Assert.ok(gTestUsedOurMock, "The test properly used the mock registry"); + // Reset gTestUsedOurMock, because we just used it. + gTestUsedOurMock = false; + // Try and get the MIME type associated with the extension. If this + // operation does not throw an unexpected exception, the test succeeds. + Assert.throws( + () => { + Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromExtension(FILE_EXTENSION); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should throw a NOT_AVAILABLE exception" + ); + + Assert.ok(gTestUsedOurMock, "The test properly used the mock registry"); +} + +/** + * Constructs a new mock registry key by wrapping the provided object. + * + * This mock implementation is tailored for this test, and forces consumers + * of the readStringValue method to believe that the "Content Type" value of + * the FILE_EXTENSION key under HKEY_CLASSES_ROOT is an empty string. + * + * The same value read from "HKEY_LOCAL_MACHINE\SOFTWARE\Classes" is not + * affected. + * + * @param aWrappedObject An actual nsIWindowsRegKey implementation. + */ +function MockWindowsRegKey(aWrappedObject) { + this._wrappedObject = aWrappedObject; + + // This function creates a forwarding function for wrappedObject + function makeForwardingFunction(functionName) { + return function () { + return aWrappedObject[functionName].apply(aWrappedObject, arguments); + }; + } + + // Forward all the functions that are not explicitly overridden + for (var propertyName in aWrappedObject) { + if (!(propertyName in this)) { + if (typeof aWrappedObject[propertyName] == "function") { + this[propertyName] = makeForwardingFunction(propertyName); + } else { + this[propertyName] = aWrappedObject[propertyName]; + } + } + } +} + +MockWindowsRegKey.prototype = { + // --- Overridden nsISupports interface functions --- + + QueryInterface: ChromeUtils.generateQI(["nsIWindowsRegKey"]), + + // --- Overridden nsIWindowsRegKey interface functions --- + + open(aRootKey, aRelPath, aMode) { + // Remember the provided root key and path + this._rootKey = aRootKey; + this._relPath = aRelPath; + + // Create the actual registry key + return this._wrappedObject.open(aRootKey, aRelPath, aMode); + }, + + openChild(aRelPath, aMode) { + // Open the child key and wrap it + var innerKey = this._wrappedObject.openChild(aRelPath, aMode); + var key = new MockWindowsRegKey(innerKey); + + // Set the properties of the child key and return it + key._rootKey = this._rootKey; + key._relPath = this._relPath + aRelPath; + return key; + }, + + createChild(aRelPath, aMode) { + // Create the child key and wrap it + var innerKey = this._wrappedObject.createChild(aRelPath, aMode); + var key = new MockWindowsRegKey(innerKey); + + // Set the properties of the child key and return it + key._rootKey = this._rootKey; + key._relPath = this._relPath + aRelPath; + return key; + }, + + get childCount() { + return this._wrappedObject.childCount; + }, + + get valueCount() { + return this._wrappedObject.valueCount; + }, + + readStringValue(aName) { + // If this is the key under test, return a fake value + if ( + this._rootKey == Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT && + this._relPath.toLowerCase() == FILE_EXTENSION && + aName.toLowerCase() == "content type" + ) { + gTestUsedOurMock = true; + return ""; + } + // Return the real value from the registry + return this._wrappedObject.readStringValue(aName); + }, +}; + +function registerMockWindowsRegKeyFactory() { + const kMockCID = Components.ID("{9b23dfe9-296b-4740-ba1c-d39c9a16e55e}"); + const kWindowsRegKeyContractID = "@mozilla.org/windows-registry-key;1"; + // Preserve the original CID. + let originalWindowsRegKeyCID = Cc[kWindowsRegKeyContractID].number; + + // See bug 1694345 - nsNotifyAddrListener::CheckAdaptersAddresses might + // attempt to use the registry off the main thread, so we disable that + // feature while the mock registry is active. + let oldSuffixListPref = Services.prefs.getBoolPref( + "network.notify.dnsSuffixList" + ); + Services.prefs.setBoolPref("network.notify.dnsSuffixList", false); + + let oldCheckForProxiesPref = Services.prefs.getBoolPref( + "network.notify.checkForProxies" + ); + Services.prefs.setBoolPref("network.notify.checkForProxies", false); + + let oldCheckForNRPTPref = Services.prefs.getBoolPref( + "network.notify.checkForNRPT" + ); + Services.prefs.setBoolPref("network.notify.checkForNRPT", false); + + info("Create a mock RegKey factory"); + let originalRegKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + let mockWindowsRegKeyFactory = { + createInstance(iid) { + info("Create a mock wrapper around RegKey"); + var key = new MockWindowsRegKey(originalRegKey); + return key.QueryInterface(iid); + }, + }; + info("Register the mock RegKey factory"); + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory( + kMockCID, + "Mock Windows Registry Key Implementation", + kWindowsRegKeyContractID, + mockWindowsRegKeyFactory + ); + + registerCleanupFunction(() => { + // Free references to the mock factory + registrar.unregisterFactory(kMockCID, mockWindowsRegKeyFactory); + // Restore the original factory + registrar.registerFactory( + Components.ID(originalWindowsRegKeyCID), + "", + kWindowsRegKeyContractID, + null + ); + + Services.prefs.setBoolPref( + "network.notify.dnsSuffixList", + oldSuffixListPref + ); + Services.prefs.setBoolPref( + "network.notify.checkForProxies", + oldCheckForProxiesPref + ); + Services.prefs.setBoolPref( + "network.notify.checkForNRPT", + oldCheckForNRPTPref + ); + }); +} diff --git a/uriloader/exthandler/tests/unit/test_handlerService.js b/uriloader/exthandler/tests/unit/test_handlerService.js new file mode 100644 index 0000000000..46a4d9fdd9 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_handlerService.js @@ -0,0 +1,467 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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() { + //* *************************************************************************// + // Constants + + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + + const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + + const protoSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + + const prefSvc = Services.prefs; + + let noMailto = false; + if (mozinfo.os == "win") { + // Check mailto handler from registry. + // If registry entry is nothing, no mailto handler + let regSvc = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + try { + regSvc.open(regSvc.ROOT_KEY_CLASSES_ROOT, "mailto", regSvc.ACCESS_READ); + noMailto = false; + } catch (ex) { + noMailto = true; + } + regSvc.close(); + } + + if (mozinfo.os == "linux") { + // Check mailto handler from GIO + // If there isn't one, then we have no mailto handler + let gIOSvc = Cc["@mozilla.org/gio-service;1"].createInstance( + Ci.nsIGIOService + ); + try { + gIOSvc.getAppForURIScheme("mailto"); + noMailto = false; + } catch (ex) { + noMailto = true; + } + } + + //* *************************************************************************// + // Sample Data + + // It doesn't matter whether or not this nsIFile is actually executable, + // only that it has a path and exists. Since we don't know any executable + // that exists on all platforms (except possibly the application being + // tested, but there doesn't seem to be a way to get a reference to that + // from the directory service), we use the temporary directory itself. + var executable = Services.dirsvc.get("TmpD", Ci.nsIFile); + // XXX We could, of course, create an actual executable in the directory: + // executable.append("localhandler"); + // if (!executable.exists()) + // executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755); + + var localHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + localHandler.name = "Local Handler"; + localHandler.executable = executable; + + var webHandler = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + webHandler.name = "Web Handler"; + webHandler.uriTemplate = "http://www.example.com/?%s"; + + // FIXME: these tests create and manipulate enough variables that it would + // make sense to move each test into its own scope so we don't run the risk + // of one test stomping on another's data. + + //* *************************************************************************// + // Test Default Properties + + // Get a handler info for a MIME type that neither the application nor + // the OS knows about and make sure its properties are set to the proper + // default values. + + var handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null); + + // Make sure it's also an nsIHandlerInfo. + Assert.ok(handlerInfo instanceof Ci.nsIHandlerInfo); + + Assert.equal(handlerInfo.type, "nonexistent/type"); + + // Deprecated property, but we should still make sure it's set correctly. + Assert.equal(handlerInfo.MIMEType, "nonexistent/type"); + + // These properties are the ones the handler service knows how to store. + Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.saveToDisk); + Assert.equal(handlerInfo.preferredApplicationHandler, null); + Assert.equal(handlerInfo.possibleApplicationHandlers.length, 0); + Assert.equal( + handlerInfo.alwaysAskBeforeHandling, + prefSvc.getBoolPref( + "browser.download.always_ask_before_handling_new_types", + false + ) + ); + + // These properties are initialized to default values by the service, + // so we might as well make sure they're initialized to the right defaults. + Assert.equal(handlerInfo.description, ""); + Assert.equal(handlerInfo.hasDefaultHandler, false); + Assert.equal(handlerInfo.defaultDescription, ""); + + const kExternalWarningDefault = + "network.protocol-handler.warn-external-default"; + prefSvc.setBoolPref(kExternalWarningDefault, true); + + // XXX add more thorough protocol info property checking + + // no OS default handler exists + var protoInfo = protoSvc.getProtocolHandlerInfo("x-moz-rheet"); + Assert.equal(protoInfo.preferredAction, protoInfo.alwaysAsk); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default does not exist, + // explicit warning pref: false + const kExternalWarningPrefPrefix = "network.protocol-handler.warn-external."; + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", false); + protoInfo = protoSvc.getProtocolHandlerInfo("http"); + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + // NOTE: this assertion will fail if the system executing the test does not + // have a handler registered for the http protocol. This is very unlikely to + // actually happen except on certain configurations of Linux, but one of + // those configurations is the default WSL Ubuntu install. So, if you are + // running this test locally and seeing a failure here, it might not be + // anything to really worry about. + Assert.ok(!protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default does not exist, + // explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", true); + protoInfo = protoSvc.getProtocolHandlerInfo("http"); + // OS handler isn't included in possibleApplicationHandlers, so length is 0 + // Once they become instances of nsILocalHandlerApp, this number will need + // to change. + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default exists, explicit warning pref: false + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (AppConstants.MOZ_APP_NAME == "thunderbird") { + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + } else { + Assert.equal(1, protoInfo.possibleApplicationHandlers.length); + } + + // Win7+ or Linux's GIO might not have a default mailto: handler + if (noMailto) { + Assert.ok(protoInfo.alwaysAskBeforeHandling); + } else { + Assert.ok(!protoInfo.alwaysAskBeforeHandling); + } + + // OS default exists, injected default exists, explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", true); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (AppConstants.MOZ_APP_NAME == "thunderbird") { + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + } else { + Assert.equal(1, protoInfo.possibleApplicationHandlers.length); + // Win7+ or Linux's GIO may have no default mailto: handler, so we'd ask + // anyway. Otherwise, the default handlers will not have stored preferred + // actions etc., so re-requesting them after the warning pref has changed + // will use the updated pref value. So both when we have and do not have + // a default mailto: handler, we'll ask: + Assert.ok(protoInfo.alwaysAskBeforeHandling); + // As soon as anyone actually stores updated defaults into the profile + // database, that default will stop tracking the warning pref. + } + // Now set the value stored in RDF to true, and the pref to false, to make + // sure we still get the right value. (Basically, same thing as above but + // with the values reversed.) + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo.alwaysAskBeforeHandling = true; + handlerSvc.store(protoInfo); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (AppConstants.MOZ_APP_NAME == "thunderbird") { + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + } else { + Assert.equal(1, protoInfo.possibleApplicationHandlers.length); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + } + + //* *************************************************************************// + // Test Round-Trip Data Integrity + + // Test round-trip data integrity by setting the properties of the handler + // info object to different values, telling the handler service to store the + // object, and then retrieving a new info object for the same type and making + // sure its properties are identical. + + handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + handlerInfo.preferredApplicationHandler = localHandler; + handlerInfo.alwaysAskBeforeHandling = false; + + handlerSvc.store(handlerInfo); + + handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null); + + Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp); + + Assert.notEqual(handlerInfo.preferredApplicationHandler, null); + var preferredHandler = handlerInfo.preferredApplicationHandler; + Assert.equal(typeof preferredHandler, "object"); + Assert.equal(preferredHandler.name, "Local Handler"); + Assert.ok(preferredHandler instanceof Ci.nsILocalHandlerApp); + preferredHandler.QueryInterface(Ci.nsILocalHandlerApp); + Assert.equal(preferredHandler.executable.path, localHandler.executable.path); + + Assert.ok(!handlerInfo.alwaysAskBeforeHandling); + + // Make sure the handler service's enumerate method lists all known handlers. + var handlerInfo2 = mimeSvc.getFromTypeAndExtension("nonexistent/type2", null); + handlerSvc.store(handlerInfo2); + var handlerTypes = ["nonexistent/type", "nonexistent/type2"]; + handlerTypes.push("mailto"); + for (let handler of handlerSvc.enumerate()) { + Assert.notEqual(handlerTypes.indexOf(handler.type), -1); + handlerTypes.splice(handlerTypes.indexOf(handler.type), 1); + } + Assert.equal(handlerTypes.length, 0); + // Make sure the handler service's remove method removes a handler record. + handlerSvc.remove(handlerInfo2); + let handlers = handlerSvc.enumerate(); + while (handlers.hasMoreElements()) { + Assert.notEqual( + handlers.getNext().QueryInterface(Ci.nsIHandlerInfo).type, + handlerInfo2.type + ); + } + + // Make sure we can store and retrieve a handler info object with no preferred + // handler. + var noPreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/no-preferred-handler", + null + ); + handlerSvc.store(noPreferredHandlerInfo); + noPreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/no-preferred-handler", + null + ); + Assert.equal(noPreferredHandlerInfo.preferredApplicationHandler, null); + + // Make sure that the handler service removes an existing handler record + // if we store a handler info object with no preferred handler. + var removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/rem-preferred-handler", + null + ); + removePreferredHandlerInfo.preferredApplicationHandler = localHandler; + handlerSvc.store(removePreferredHandlerInfo); + removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/rem-preferred-handler", + null + ); + removePreferredHandlerInfo.preferredApplicationHandler = null; + handlerSvc.store(removePreferredHandlerInfo); + removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/rem-preferred-handler", + null + ); + Assert.equal(removePreferredHandlerInfo.preferredApplicationHandler, null); + + // Make sure we can store and retrieve a handler info object with possible + // handlers. We test both adding and removing handlers. + + // Get a handler info and make sure it has no possible handlers. + var possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Store and re-retrieve the handler and make sure it still has no possible + // handlers. + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Add two handlers, store the object, re-retrieve it, and make sure it has + // two handlers. + possibleHandlersInfo.possibleApplicationHandlers.appendElement(localHandler); + possibleHandlersInfo.possibleApplicationHandlers.appendElement(webHandler); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 2); + + // Figure out which is the local and which is the web handler and the index + // in the array of the local handler, which is the one we're going to remove + // to test removal of a handler. + var handler1 = + possibleHandlersInfo.possibleApplicationHandlers.queryElementAt( + 0, + Ci.nsIHandlerApp + ); + var handler2 = + possibleHandlersInfo.possibleApplicationHandlers.queryElementAt( + 1, + Ci.nsIHandlerApp + ); + var localPossibleHandler, webPossibleHandler, localIndex; + if (handler1 instanceof Ci.nsILocalHandlerApp) { + [localPossibleHandler, webPossibleHandler, localIndex] = [ + handler1, + handler2, + 0, + ]; + } else { + [localPossibleHandler, webPossibleHandler, localIndex] = [ + handler2, + handler1, + 1, + ]; + } + localPossibleHandler.QueryInterface(Ci.nsILocalHandlerApp); + webPossibleHandler.QueryInterface(Ci.nsIWebHandlerApp); + + // Make sure the two handlers are the ones we stored. + Assert.equal(localPossibleHandler.name, localHandler.name); + Assert.ok(localPossibleHandler.equals(localHandler)); + Assert.equal(webPossibleHandler.name, webHandler.name); + Assert.ok(webPossibleHandler.equals(webHandler)); + + // Remove a handler, store the object, re-retrieve it, and make sure + // it only has one handler. + possibleHandlersInfo.possibleApplicationHandlers.removeElementAt(localIndex); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 1); + + // Make sure the handler is the one we didn't remove. + webPossibleHandler = + possibleHandlersInfo.possibleApplicationHandlers.queryElementAt( + 0, + Ci.nsIWebHandlerApp + ); + Assert.equal(webPossibleHandler.name, webHandler.name); + Assert.ok(webPossibleHandler.equals(webHandler)); + + // //////////////////////////////////////////////////// + // handler info command line parameters and equality + var localApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + var handlerApp = localApp.QueryInterface(Ci.nsIHandlerApp); + + Assert.ok(handlerApp.equals(localApp)); + + localApp.executable = executable; + + Assert.equal(0, localApp.parameterCount); + localApp.appendParameter("-test1"); + Assert.equal(1, localApp.parameterCount); + localApp.appendParameter("-test2"); + Assert.equal(2, localApp.parameterCount); + Assert.ok(localApp.parameterExists("-test1")); + Assert.ok(localApp.parameterExists("-test2")); + Assert.ok(!localApp.parameterExists("-false")); + localApp.clearParameters(); + Assert.equal(0, localApp.parameterCount); + + var localApp2 = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + + localApp2.executable = executable; + + localApp.clearParameters(); + Assert.ok(localApp.equals(localApp2)); + + // equal: + // cut -d 1 -f 2 + // cut -d 1 -f 2 + + localApp.appendParameter("-test1"); + localApp.appendParameter("-test2"); + localApp.appendParameter("-test3"); + localApp2.appendParameter("-test1"); + localApp2.appendParameter("-test2"); + localApp2.appendParameter("-test3"); + Assert.ok(localApp.equals(localApp2)); + + // not equal: + // cut -d 1 -f 2 + // cut -f 1 -d 2 + + localApp.clearParameters(); + localApp2.clearParameters(); + + localApp.appendParameter("-test1"); + localApp.appendParameter("-test2"); + localApp.appendParameter("-test3"); + localApp2.appendParameter("-test2"); + localApp2.appendParameter("-test1"); + localApp2.appendParameter("-test3"); + Assert.ok(!localApp2.equals(localApp)); + + var str; + str = localApp.getParameter(0); + Assert.equal(str, "-test1"); + str = localApp.getParameter(1); + Assert.equal(str, "-test2"); + str = localApp.getParameter(2); + Assert.equal(str, "-test3"); + + // FIXME: test round trip integrity for a protocol. + // FIXME: test round trip integrity for a handler info with a web handler. + + //* *************************************************************************// + // getTypeFromExtension tests + + // test nonexistent extension + var lolType = handlerSvc.getTypeFromExtension("lolcat"); + Assert.equal(lolType, ""); + + // add a handler for the extension + var lolHandler = mimeSvc.getFromTypeAndExtension("application/lolcat", null); + + Assert.ok(!lolHandler.extensionExists("lolcat")); + lolHandler.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + lolHandler.preferredApplicationHandler = localHandler; + lolHandler.alwaysAskBeforeHandling = false; + lolHandler.appendExtension("lolcat"); + + // store the handler + Assert.ok(!handlerSvc.exists(lolHandler)); + handlerSvc.store(lolHandler); + Assert.ok(handlerSvc.exists(lolHandler)); + + // test now-existent extension + lolType = handlerSvc.getTypeFromExtension("lolcat"); + Assert.equal(lolType, "application/lolcat"); + + // test mailcap entries with needsterminal are ignored on non-Windows non-Mac. + if (mozinfo.os != "win" && mozinfo.os != "mac") { + prefSvc.setStringPref( + "helpers.private_mailcap_file", + do_get_file("mailcap").path + ); + handlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", null); + Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.saveToDisk); + Assert.equal(handlerInfo.defaultDescription, "sed"); + } +} diff --git a/uriloader/exthandler/tests/unit/test_handlerService_store.js b/uriloader/exthandler/tests/unit/test_handlerService_store.js new file mode 100644 index 0000000000..03573dbc2c --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_handlerService_store.js @@ -0,0 +1,752 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests the nsIHandlerService interface. + */ + +// Set up an nsIWebHandlerApp instance that can be used in multiple tests. +let webHandlerApp = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" +].createInstance(Ci.nsIWebHandlerApp); +webHandlerApp.name = "Web Handler"; +webHandlerApp.uriTemplate = "https://www.example.com/?url=%s"; +let expectedWebHandlerApp = { + name: webHandlerApp.name, + uriTemplate: webHandlerApp.uriTemplate, +}; + +// Set up an nsILocalHandlerApp instance that can be used in multiple tests. The +// executable should exist, but it doesn't need to point to an actual file, so +// we simply initialize it to the path of an existing directory. +let localHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" +].createInstance(Ci.nsILocalHandlerApp); +localHandlerApp.name = "Local Handler"; +localHandlerApp.executable = FileUtils.getFile("TmpD", []); +let expectedLocalHandlerApp = { + name: localHandlerApp.name, + executable: localHandlerApp.executable, +}; + +/** + * Returns a new nsIHandlerInfo instance initialized to known values that don't + * depend on the platform and are easier to verify later. + * + * @param type + * Because the "preferredAction" is initialized to saveToDisk, this + * should represent a MIME type rather than a protocol. + */ +function getKnownHandlerInfo(type) { + let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(type); + handlerInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk; + handlerInfo.alwaysAskBeforeHandling = false; + return handlerInfo; +} + +/** + * Checks that the information stored in the handler service instance under + * testing matches the test data files. + */ +function assertAllHandlerInfosMatchTestData() { + let handlerInfos = HandlerServiceTestUtils.getAllHandlerInfos(); + + // It's important that the MIME types we check here do not exist at the + // operating system level, otherwise the list of handlers and file extensions + // will be merged. The current implementation avoids duplicate entries. + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.handleinternally", + preferredAction: Ci.nsIHandlerInfo.handleInternally, + alwaysAskBeforeHandling: false, + fileExtensions: ["example_one"], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.savetodisk", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: true, + preferredApplicationHandler: { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + possibleApplicationHandlers: [ + { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + ], + fileExtensions: ["example_two", "example_three"], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.usehelperapp", + preferredAction: Ci.nsIHandlerInfo.useHelperApp, + alwaysAskBeforeHandling: true, + preferredApplicationHandler: { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + possibleApplicationHandlers: [ + { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + { + name: "Example Possible Handler One", + uriTemplate: "http://www.example.com/?id=1&url=%s", + }, + { + name: "Example Possible Handler Two", + uriTemplate: "http://www.example.com/?id=2&url=%s", + }, + ], + fileExtensions: ["example_two", "example_three"], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.usesystemdefault", + preferredAction: Ci.nsIHandlerInfo.useSystemDefault, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [ + { + name: "Example Possible Handler", + uriTemplate: "http://www.example.com/?url=%s", + }, + ], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "examplescheme.usehelperapp", + preferredAction: Ci.nsIHandlerInfo.useHelperApp, + alwaysAskBeforeHandling: true, + preferredApplicationHandler: { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + possibleApplicationHandlers: [ + { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + { + name: "Example Possible Handler One", + uriTemplate: "http://www.example.com/?id=1&url=%s", + }, + { + name: "Example Possible Handler Two", + uriTemplate: "http://www.example.com/?id=2&url=%s", + }, + ], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "examplescheme.usesystemdefault", + preferredAction: Ci.nsIHandlerInfo.useSystemDefault, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [ + { + name: "Example Possible Handler", + uriTemplate: "http://www.example.com/?url=%s", + }, + ], + }); + + Assert.equal(handlerInfos.length, 0); +} + +/** + * Loads data from a file in a predefined format, verifying that the format is + * recognized and all the known properties are loaded and saved. + */ +add_task(async function test_store_fillHandlerInfo_predefined() { + // Test that the file format used in previous versions can be loaded. + await copyTestDataToHandlerStore(); + await assertAllHandlerInfosMatchTestData(); + + // Keep a copy of the nsIHandlerInfo instances, then delete the handler store + // and populate it with the known data. Since the handler store is empty, the + // default handlers for the current locale are also injected, so we have to + // delete them manually before adding the other nsIHandlerInfo instances. + let testHandlerInfos = HandlerServiceTestUtils.getAllHandlerInfos(); + await deleteHandlerStore(); + for (let handlerInfo of HandlerServiceTestUtils.getAllHandlerInfos()) { + gHandlerService.remove(handlerInfo); + } + for (let handlerInfo of testHandlerInfos) { + gHandlerService.store(handlerInfo); + } + + // Test that the known data still matches after saving it and reloading. + await unloadHandlerStore(); + await assertAllHandlerInfosMatchTestData(); +}); + +/** + * Check that "store" is able to add new instances, that "remove" and "exists" + * work, and that "fillHandlerInfo" throws when the instance does not exist. + */ +add_task(async function test_store_remove_exists() { + // Test both MIME types and protocols. + for (let type of [ + "example/type.usehelperapp", + "examplescheme.usehelperapp", + ]) { + // Create new nsIHandlerInfo instances before loading the test data. + await deleteHandlerStore(); + let handlerInfoPresent = HandlerServiceTestUtils.getHandlerInfo(type); + let handlerInfoAbsent = HandlerServiceTestUtils.getHandlerInfo(type + "2"); + + // Set up known properties that we can verify later. + handlerInfoAbsent.preferredAction = Ci.nsIHandlerInfo.saveToDisk; + handlerInfoAbsent.alwaysAskBeforeHandling = false; + + await copyTestDataToHandlerStore(); + + Assert.ok(gHandlerService.exists(handlerInfoPresent)); + Assert.ok(!gHandlerService.exists(handlerInfoAbsent)); + + gHandlerService.store(handlerInfoAbsent); + gHandlerService.remove(handlerInfoPresent); + + await unloadHandlerStore(); + + Assert.ok(!gHandlerService.exists(handlerInfoPresent)); + Assert.ok(gHandlerService.exists(handlerInfoAbsent)); + + Assert.throws( + () => gHandlerService.fillHandlerInfo(handlerInfoPresent, ""), + ex => ex.result == Cr.NS_ERROR_NOT_AVAILABLE + ); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo(type + "2"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: type + "2", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + }); + } +}); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with a + * "preferredAction" that is either a valid or an unknown value, and the + * action always takes on an appropriate value when reloading. + */ +add_task(async function test_store_preferredAction() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + // Valid action values should all remain unchanged across a refresh, except + // for alwaysAsk which may be overridden with useHelperApp depending on prefs. + // Invalid action values should always convert to useHelperApp. + const actions = [ + { + preferred: Ci.nsIHandlerInfo.alwaysAsk, + expected: Ci.nsIHandlerInfo.alwaysAsk, + }, + { + preferred: Ci.nsIHandlerInfo.handleInternally, + expected: Ci.nsIHandlerInfo.handleInternally, + }, + { preferred: 999, expected: Ci.nsIHandlerInfo.useHelperApp }, + ]; + + for (let action of actions) { + handlerInfo.preferredAction = action.preferred; + gHandlerService.store(handlerInfo); + gHandlerService.fillHandlerInfo(handlerInfo, ""); + Assert.equal(handlerInfo.preferredAction, action.expected); + } +}); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance containing an + * nsILocalHandlerApp instance pointing to an executable that doesn't exist, but + * this entry is ignored when reloading. + */ +add_task(async function test_store_localHandlerApp_missing() { + if (!("@mozilla.org/uriloader/dbus-handler-app;1" in Cc)) { + info("Skipping test because it does not apply to this platform."); + return; + } + + let missingHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + missingHandlerApp.name = "Non-existing Handler"; + missingHandlerApp.executable = FileUtils.getFile("TmpD", ["nonexisting"]); + + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = missingHandlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(missingHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [expectedWebHandlerApp], + }); +}); + +/** + * Test saving and reloading an instance of nsIDBusHandlerApp. + */ +add_task(async function test_store_dBusHandlerApp() { + if (!("@mozilla.org/uriloader/dbus-handler-app;1" in Cc)) { + info("Skipping test because it does not apply to this platform."); + return; + } + + // Set up an nsIDBusHandlerApp instance for testing. + let dBusHandlerApp = Cc[ + "@mozilla.org/uriloader/dbus-handler-app;1" + ].createInstance(Ci.nsIDBusHandlerApp); + dBusHandlerApp.name = "DBus Handler"; + dBusHandlerApp.service = "test.method.server"; + dBusHandlerApp.method = "Method"; + dBusHandlerApp.dBusInterface = "test.method.Type"; + dBusHandlerApp.objectPath = "/test/method/Object"; + let expectedDBusHandlerApp = { + name: dBusHandlerApp.name, + service: dBusHandlerApp.service, + method: dBusHandlerApp.method, + dBusInterface: dBusHandlerApp.dBusInterface, + objectPath: dBusHandlerApp.objectPath, + }; + + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = dBusHandlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(dBusHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedDBusHandlerApp, + possibleApplicationHandlers: [expectedDBusHandlerApp], + }); +}); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with a + * "preferredApplicationHandler" and no "possibleApplicationHandlers", but the + * former is always included in the latter list when reloading. + */ +add_task( + async function test_store_possibleApplicationHandlers_includes_preferred() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = localHandlerApp; + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = + HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedLocalHandlerApp, + possibleApplicationHandlers: [expectedLocalHandlerApp], + }); + } +); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with a + * "preferredApplicationHandler" that is not the first element in + * "possibleApplicationHandlers", but the former is always included as the first + * element of the latter list when reloading. + */ +add_task( + async function test_store_possibleApplicationHandlers_preferred_first() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = webHandlerApp; + // The preferred handler is appended after the other one. + handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = + HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedWebHandlerApp, + possibleApplicationHandlers: [ + expectedWebHandlerApp, + expectedLocalHandlerApp, + ], + }); + } +); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with an + * uppercase file extension, but it is converted to lowercase when reloading. + */ +add_task(async function test_store_fileExtensions_lowercase() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.appendExtension("extension_test1"); + handlerInfo.appendExtension("EXTENSION_test2"); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + fileExtensions: ["extension_test1", "extension_test2"], + }); +}); + +/** + * Tests that appendExtension doesn't add duplicates, and that anyway duplicates + * from possibleApplicationHandlers are removed when saving and reloading. + */ +add_task(async function test_store_no_duplicates() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = webHandlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + handlerInfo.appendExtension("extension_test1"); + handlerInfo.appendExtension("extension_test2"); + handlerInfo.appendExtension("extension_test1"); + handlerInfo.appendExtension("EXTENSION_test1"); + Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), [ + "extension_test1", + "extension_test2", + ]); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedWebHandlerApp, + possibleApplicationHandlers: [ + expectedWebHandlerApp, + expectedLocalHandlerApp, + ], + fileExtensions: ["extension_test1", "extension_test2"], + }); +}); + +/** + * Tests that setFileExtensions doesn't add duplicates. + */ +add_task(async function test_setFileExtensions_no_duplicates() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.setFileExtensions("a,b,A,b,c,a"); + let expected = ["a", "b", "c"]; + Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), expected); + // Test empty extensions, also at begin and end. + handlerInfo.setFileExtensions(",a,,b,A,c,"); + Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), expected); +}); + +/** + * Tests that "store" deletes properties that have their default values from + * the data store. + * + * File extensions are never deleted once they have been associated. + */ +add_task(async function test_store_deletes_properties_except_extensions() { + await deleteHandlerStore(); + + // Prepare an nsIHandlerInfo instance with all the properties set to values + // that will result in deletions. The preferredAction is also set to a defined + // value so we can more easily verify it later. + let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo( + "example/type.savetodisk" + ); + handlerInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk; + handlerInfo.alwaysAskBeforeHandling = false; + + // All the properties for "example/type.savetodisk" are present in the test + // data, so we load the data before overwriting their values. + await copyTestDataToHandlerStore(); + gHandlerService.store(handlerInfo); + + // Now we can reload the data and verify that no extra values have been kept. + await unloadHandlerStore(); + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo( + "example/type.savetodisk" + ); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/type.savetodisk", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + fileExtensions: ["example_two", "example_three"], + }); +}); + +/** + * Tests the "overrideType" argument of "fillHandlerInfo". + */ +add_task(async function test_fillHandlerInfo_overrideType() { + // Test both MIME types and protocols. + for (let type of [ + "example/type.usesystemdefault", + "examplescheme.usesystemdefault", + ]) { + await deleteHandlerStore(); + + // Create new nsIHandlerInfo instances before loading the test data. + let handlerInfoAbsent = HandlerServiceTestUtils.getHandlerInfo(type + "2"); + + // Fill the nsIHandlerInfo instance using the type that actually exists. + await copyTestDataToHandlerStore(); + gHandlerService.fillHandlerInfo(handlerInfoAbsent, type); + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfoAbsent, { + // While the data is populated from another type, the type is unchanged. + type: type + "2", + preferredAction: Ci.nsIHandlerInfo.useSystemDefault, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [ + { + name: "Example Possible Handler", + uriTemplate: "http://www.example.com/?url=%s", + }, + ], + }); + } +}); + +/** + * Tests "getTypeFromExtension" including unknown extensions. + */ +add_task(async function test_getTypeFromExtension() { + await copyTestDataToHandlerStore(); + + Assert.equal(gHandlerService.getTypeFromExtension(""), ""); + Assert.equal(gHandlerService.getTypeFromExtension("example_unknown"), ""); + Assert.equal( + gHandlerService.getTypeFromExtension("example_one"), + "example/type.handleinternally" + ); + Assert.equal( + gHandlerService.getTypeFromExtension("EXAMPLE_one"), + "example/type.handleinternally" + ); +}); + +/** + * Checks that the information stored in the handler service instance under + * testing matches the default handlers for the English locale. + */ +function assertAllHandlerInfosMatchDefaultHandlers() { + let handlerInfos = HandlerServiceTestUtils.getAllHandlerInfos(); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "mailto", + preferredActionOSDependent: true, + possibleApplicationHandlers: [ + { + name: "Gmail", + uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s", + }, + ], + }); + + Assert.equal(handlerInfos.length, 0); +} + +/** + * Tests the default protocol handlers imported from the locale-specific data. + */ +add_task( + { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" }, + async function test_default_protocol_handlers() { + if ( + !Services.prefs.getPrefType("gecko.handlerService.defaultHandlersVersion") + ) { + info("This platform or locale does not have default handlers."); + return; + } + + // This will inject the default protocol handlers for the current locale. + await deleteHandlerStore(); + + await assertAllHandlerInfosMatchDefaultHandlers(); + } +); + +/** + * Tests that the default protocol handlers are not imported again from the + * locale-specific data if they already exist. + */ +add_task( + { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" }, + async function test_default_protocol_handlers_no_duplicates() { + if ( + !Services.prefs.getPrefType("gecko.handlerService.defaultHandlersVersion") + ) { + info("This platform or locale does not have default handlers."); + return; + } + + // This will inject the default protocol handlers for the current locale. + await deleteHandlerStore(); + + // Clear the preference to force injecting again. + Services.prefs.clearUserPref("gecko.handlerService.defaultHandlersVersion"); + + await unloadHandlerStore(); + + // There should be no duplicate handlers in the protocols. + assertAllHandlerInfosMatchDefaultHandlers(); + } +); + +/** + * Ensures forward compatibility by checking that the "store" method preserves + * unknown properties in the test data. + */ +add_task(async function test_store_keeps_unknown_properties() { + // Create a new nsIHandlerInfo instance before loading the test data. + await deleteHandlerStore(); + let handlerInfo = HandlerServiceTestUtils.getHandlerInfo( + "example/type.handleinternally" + ); + + await copyTestDataToHandlerStore(); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + let data = await IOUtils.readJSON(jsonPath); + Assert.equal( + data.mimeTypes["example/type.handleinternally"].unknownProperty, + "preserved" + ); +}); + +/** + * Runs the asyncInit method, ensuring that it successfully inits the store + * and calls the handlersvc-store-initialized topic. + */ +add_task(async function test_async_init() { + await deleteHandlerStore(); + await copyTestDataToHandlerStore(); + gHandlerService.asyncInit(); + await TestUtils.topicObserved("handlersvc-store-initialized"); + await assertAllHandlerInfosMatchTestData(); + + await unloadHandlerStore(); +}); + +/** + * Races the asyncInit method against the sync init (implicit in enumerate), + * to ensure that the store will be synchronously initialized without any + * ill effects. + */ +add_task(async function test_race_async_init() { + await deleteHandlerStore(); + await copyTestDataToHandlerStore(); + let storeInitialized = false; + // Pass a callback to synchronously observe the topic, as a promise would + // resolve asynchronously + TestUtils.topicObserved("handlersvc-store-initialized", () => { + storeInitialized = true; + return true; + }); + gHandlerService.asyncInit(); + Assert.ok(!storeInitialized); + gHandlerService.enumerate(); + Assert.ok(storeInitialized); + await assertAllHandlerInfosMatchTestData(); + + await unloadHandlerStore(); +}); + +/** + * Test saving and reloading an instance of nsIGIOMimeApp. + */ +add_task(async function test_store_gioHandlerApp() { + if (!("@mozilla.org/gio-service;1" in Cc)) { + info("Skipping test because it does not apply to this platform."); + return; + } + + // Create dummy exec file that following won't fail because file not found error + let dummyHandlerFile = FileUtils.getFile("TmpD", ["dummyHandler"]); + dummyHandlerFile.createUnique( + Ci.nsIFile.NORMAL_FILE_TYPE, + parseInt("777", 8) + ); + + // Set up an nsIGIOMimeApp instance for testing. + let handlerApp = Cc["@mozilla.org/gio-service;1"] + .getService(Ci.nsIGIOService) + .createAppFromCommand(dummyHandlerFile.path, "Dummy GIO handler"); + let expectedGIOMimeHandlerApp = { + name: handlerApp.name, + command: handlerApp.command, + }; + + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = handlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(handlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedGIOMimeHandlerApp, + possibleApplicationHandlers: [expectedGIOMimeHandlerApp, webHandlerApp], + }); + + await IOUtils.remove(dummyHandlerFile.path); + + // After removing dummyHandlerFile, the handler should disappear from the + // list of possibleApplicationHandlers and preferredAppHandler should be null. + actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: null, + possibleApplicationHandlers: [webHandlerApp], + }); +}); diff --git a/uriloader/exthandler/tests/unit/test_punycodeURIs.js b/uriloader/exthandler/tests/unit/test_punycodeURIs.js new file mode 100644 index 0000000000..949c9914ec --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_punycodeURIs.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/. */ + +// Encoded test URI to work on all platforms/independent of file encoding +const kTestURI = "http://\u65e5\u672c\u8a93.jp/"; +const kExpectedURI = "http://xn--wgv71a309e.jp/"; +const kOutputFile = "result.txt"; + +// Try several times in case the box we're running on is slow. +const kMaxCheckExistAttempts = 30; // seconds +var gCheckExistsAttempts = 0; + +const tempDir = do_get_tempdir(); + +function checkFile() { + // This is where we expect the output + var tempFile = tempDir.clone(); + tempFile.append(kOutputFile); + + if (!tempFile.exists()) { + if (gCheckExistsAttempts >= kMaxCheckExistAttempts) { + do_throw( + "Expected File " + + tempFile.path + + " does not exist after " + + kMaxCheckExistAttempts + + " seconds" + ); + } else { + ++gCheckExistsAttempts; + // Wait a bit longer then try again + do_timeout(1000, checkFile); + return; + } + } + + // Now read it + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + fstream.init(tempFile, -1, 0, 0); + sstream.init(fstream); + + // Read the first line only as that's the one we expect WriteArguments + // to be writing the argument to. + var data = sstream.read(4096); + + sstream.close(); + fstream.close(); + + // Now remove the old file + tempFile.remove(false); + + // This currently fails on Mac with an argument like -psn_0_nnnnnn + // This seems to be to do with how the executable is called, but I couldn't + // find a way around it. + // Additionally the lack of OS detection in xpcshell tests sucks, so we'll + // have to check for the argument mac gives us. + if (data.substring(0, 7) != "-psn_0_") { + Assert.equal(data, kExpectedURI); + } + + do_test_finished(); +} + +function run_test() { + if (mozinfo.os == "mac") { + dump("INFO | test_punycodeURIs.js | Skipping test on mac, bug 599475"); + return; + } + + // set up the uri to test with + var ioService = Services.io; + + // set up the local handler object + var localHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + localHandler.name = "Test Local Handler App"; + + // WriteArgument will just dump its arguments to a file for us. + var processDir = do_get_cwd(); + var exe = processDir.clone(); + exe.append("WriteArgument"); + + if (!exe.exists()) { + // Maybe we are on windows + exe.leafName = "WriteArgument.exe"; + if (!exe.exists()) { + do_throw("Could not locate the WriteArgument tests executable\n"); + } + } + + var outFile = tempDir.clone(); + outFile.append(kOutputFile); + + // Set an environment variable for WriteArgument to pick up + // The Write Argument file needs to know where its libraries are, so + // just force the path variable + // For mac + var greDir = Services.dirsvc.get("GreD", Ci.nsIFile); + + Services.env.set("DYLD_LIBRARY_PATH", greDir.path); + // For Linux + Services.env.set("LD_LIBRARY_PATH", greDir.path); + // XXX: handle windows + + // Now tell it where we want the file. + Services.env.set("WRITE_ARGUMENT_FILE", outFile.path); + + var uri = ioService.newURI(kTestURI); + + // Just check we've got these matching, if we haven't there's a problem + // with ascii spec or our test case. + Assert.equal(uri.asciiSpec, kExpectedURI); + + localHandler.executable = exe; + localHandler.launchWithURI(uri); + + do_test_pending(); + do_timeout(1000, checkFile); +} diff --git a/uriloader/exthandler/tests/unit/xpcshell.ini b/uriloader/exthandler/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..209cfb836a --- /dev/null +++ b/uriloader/exthandler/tests/unit/xpcshell.ini @@ -0,0 +1,38 @@ +[DEFAULT] +head = head.js +run-sequentially = Bug 912235 - Intermittent failures +firefox-appdir = browser + +[test_defaults_handlerService.js] +# No default stored handlers on android given lack of support. +# No default stored handlers on Thunderbird. +skip-if = + os == "android" + appname == "thunderbird" +[test_downloads_improvements_migration.js] +# No default stored handlers on android given lack of support. +# No default stored handlers on Thunderbird. +skip-if = + os == "android" + appname == "thunderbird" +[test_filename_sanitize.js] +[test_getFromTypeAndExtension.js] +[test_getMIMEInfo_pdf.js] +[test_getMIMEInfo_unknown_mime_type.js] +run-if = os == "win" # Windows only test +[test_getTypeFromExtension_ext_to_type_mapping.js] +[test_getTypeFromExtension_with_empty_Content_Type.js] +run-if = os == "win" # Windows only test +[test_badMIMEType.js] +run-if = buildapp == "browser" +[test_handlerService.js] +support-files = mailcap +# Bug 676997: test consistently fails on Android +fail-if = os == "android" +[test_handlerService_store.js] +# Disabled for 1563343 -- the app should determine possible handlers in GV. +fail-if = os == "android" +support-files = handlers.json +[test_punycodeURIs.js] +skip-if = + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1809485 diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h new file mode 100644 index 0000000000..06b31a723a --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 nslocalhandlerappuikit_h_ +#define nslocalhandlerappuikit_h_ + +#include "nsLocalHandlerApp.h" + +class nsLocalHandlerAppUIKit final : public nsLocalHandlerApp { + public: + nsLocalHandlerAppUIKit() {} + ~nsLocalHandlerAppUIKit() {} + + nsLocalHandlerAppUIKit(const char16_t* aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) {} + + nsLocalHandlerAppUIKit(const nsAString& aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) {} + + NS_IMETHOD LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) override; +}; + +#endif /* nslocalhandlerappuikit_h_ */ diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm new file mode 100644 index 0000000000..ae6697f9b6 --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 + +#include "nsLocalHandlerAppUIKit.h" +#include "nsIURI.h" + +NS_IMETHODIMP +nsLocalHandlerAppUIKit::LaunchWithURI(nsIURI* aURI, + mozilla::dom::BrowsingContext* aBrowsingContext) { + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h new file mode 100644 index 0000000000..109eb16e3f --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 nsMIMEInfoUIKit_h_ +#define nsMIMEInfoUIKit_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoUIKit final : public nsMIMEInfoImpl { + public: + explicit nsMIMEInfoUIKit(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) {} + nsMIMEInfoUIKit(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override; + + protected: + virtual nsresult LoadUriInternal(nsIURI* aURI) override; +#ifdef DEBUG + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) override { + MOZ_ASSERT_UNREACHABLE("do not call this method, use LaunchWithFile"); + return NS_ERROR_UNEXPECTED; + } +#endif +}; + +#endif diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm new file mode 100644 index 0000000000..2ed0c1eb2e --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "nsMIMEInfoUIKit.h" + +NS_IMETHODIMP +nsMIMEInfoUIKit::LaunchWithFile(nsIFile* aFile) { return NS_ERROR_NOT_IMPLEMENTED; } + +nsresult nsMIMEInfoUIKit::LoadUriInternal(nsIURI* aURI) { return NS_ERROR_NOT_IMPLEMENTED; } diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.h b/uriloader/exthandler/uikit/nsOSHelperAppService.h new file mode 100644 index 0000000000..426d8bb78a --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and +// is implemented on each platform. It contains platform specific code for +// finding helper applications for a given mime type in addition to launching +// those applications. This is the UIKit version. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" + +class nsOSHelperAppService final : public nsExternalHelperAppService { + public: + nsOSHelperAppService(); + ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + // method overrides --> used to hook the mime service into internet config.... + NS_IMETHOD GetFromTypeAndExtension(const nsACString& aType, + const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo) override; + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) override; + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got + // out of the rdf data source. This can be a mac file + // spec, a unix path or a windows path depending on the + // platform + // aFile --> an nsIFile representation of that platform application path. + virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile) override; + + nsresult OSProtocolHandlerExists(const char* aScheme, + bool* aHandlerExists) override; +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.mm b/uriloader/exthandler/uikit/nsOSHelperAppService.mm new file mode 100644 index 0000000000..5d6cc7b0fd --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.mm @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "nsOSHelperAppService.h" + +nsOSHelperAppService::nsOSHelperAppService() : nsExternalHelperAppService() {} + +nsOSHelperAppService::~nsOSHelperAppService() {} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) { + *aHandlerExists = false; + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, nsAString& _retval) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, bool* _retval) { + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsOSHelperAppService::GetFileTokenForPath(const char16_t* aPlatformAppPath, + nsIFile** aFile) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetFromTypeAndExtension(const nsACString& aType, const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo) { + return nsExternalHelperAppService::GetFromTypeAndExtension(aType, aFileExt, aMIMEInfo); +} + +NS_IMETHODIMP nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + *aMIMEInfo = nullptr; + *aFound = false; + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, bool* found, + nsIHandlerInfo** _retval) { + *found = false; + return NS_OK; +} diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.cpp b/uriloader/exthandler/unix/nsGNOMERegistry.cpp new file mode 100644 index 0000000000..dad4adbfd7 --- /dev/null +++ b/uriloader/exthandler/unix/nsGNOMERegistry.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/. */ + +#include "nsGNOMERegistry.h" +#include "nsString.h" +#include "nsMIMEInfoUnix.h" +#include "nsIGIOService.h" + +/* static */ +bool nsGNOMERegistry::HandlerExists(const char* aProtocolScheme) { + nsCOMPtr giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return false; + } + + nsCOMPtr app; + return NS_SUCCEEDED(giovfs->GetAppForURIScheme( + nsDependentCString(aProtocolScheme), getter_AddRefs(app))); +} + +// XXX Check HandlerExists() before calling LoadURL. + +/* static */ +nsresult nsGNOMERegistry::LoadURL(nsIURI* aURL) { + nsCOMPtr giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return NS_ERROR_FAILURE; + } + + return giovfs->ShowURI(aURL); +} + +/* static */ +void nsGNOMERegistry::GetAppDescForScheme(const nsACString& aScheme, + nsAString& aDesc) { + nsCOMPtr giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) return; + + nsCOMPtr app; + if (NS_FAILED(giovfs->GetAppForURIScheme(aScheme, getter_AddRefs(app)))) + return; + + app->GetName(aDesc); +} + +/* static */ +already_AddRefed nsGNOMERegistry::GetFromExtension( + const nsACString& aFileExt) { + nsAutoCString mimeType; + nsCOMPtr giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return nullptr; + } + + // Get the MIME type from the extension, then call GetFromType to + // fill in the MIMEInfo. + if (NS_FAILED(giovfs->GetMimeTypeFromExtension(aFileExt, mimeType)) || + mimeType.EqualsLiteral("application/octet-stream")) { + return nullptr; + } + + RefPtr mi = GetFromType(mimeType); + if (mi) { + mi->AppendExtension(aFileExt); + } + + return mi.forget(); +} + +/* static */ +already_AddRefed nsGNOMERegistry::GetFromType( + const nsACString& aMIMEType) { + RefPtr mimeInfo = new nsMIMEInfoUnix(aMIMEType); + NS_ENSURE_TRUE(mimeInfo, nullptr); + + nsAutoString name; + nsAutoCString description; + + nsCOMPtr giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return nullptr; + } + + nsCOMPtr handlerApp; + if (NS_FAILED( + giovfs->GetAppForMimeType(aMIMEType, getter_AddRefs(handlerApp))) || + !handlerApp) { + return nullptr; + } + handlerApp->GetName(name); + giovfs->GetDescriptionForMimeType(aMIMEType, description); + + mimeInfo->SetDefaultDescription(name); + + mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + mimeInfo->SetDescription(NS_ConvertUTF8toUTF16(description)); + + return mimeInfo.forget(); +} diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.h b/uriloader/exthandler/unix/nsGNOMERegistry.h new file mode 100644 index 0000000000..ea626c5b50 --- /dev/null +++ b/uriloader/exthandler/unix/nsGNOMERegistry.h @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 nsGNOMERegistry_h +#define nsGNOMERegistry_h + +#include "nsIURI.h" +#include "nsCOMPtr.h" + +class nsMIMEInfoBase; + +class nsGNOMERegistry { + public: + static bool HandlerExists(const char* aProtocolScheme); + + static nsresult LoadURL(nsIURI* aURL); + + static void GetAppDescForScheme(const nsACString& aScheme, nsAString& aDesc); + + static already_AddRefed GetFromExtension( + const nsACString& aFileExt); + + static already_AddRefed GetFromType( + const nsACString& aMIMEType); +}; + +#endif // nsGNOMERegistry_h diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp new file mode 100644 index 0000000000..330c441159 --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 3; 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 "nsMIMEInfoUnix.h" +#include "nsGNOMERegistry.h" +#include "nsIGIOService.h" +#include "nsNetCID.h" +#include "nsIIOService.h" +#ifdef MOZ_ENABLE_DBUS +# include "nsDBusHandlerApp.h" +#endif + +nsresult nsMIMEInfoUnix::LoadUriInternal(nsIURI* aURI) { + return nsGNOMERegistry::LoadURL(aURI); +} + +NS_IMETHODIMP +nsMIMEInfoUnix::GetHasDefaultHandler(bool* _retval) { + // if a default app is set, it means the application has been set from + // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to + // give the GNOME answer. + if (GetDefaultApplication()) { + return nsMIMEInfoImpl::GetHasDefaultHandler(_retval); + } + + *_retval = false; + + if (mClass == eProtocolInfo) { + *_retval = nsGNOMERegistry::HandlerExists(mSchemeOrType.get()); + } else { + RefPtr mimeInfo = + nsGNOMERegistry::GetFromType(mSchemeOrType); + if (!mimeInfo) { + nsAutoCString ext; + nsresult rv = GetPrimaryExtension(ext); + if (NS_SUCCEEDED(rv)) { + mimeInfo = nsGNOMERegistry::GetFromExtension(ext); + } + } + if (mimeInfo) *_retval = true; + } + + if (*_retval) return NS_OK; + + return NS_OK; +} + +nsresult nsMIMEInfoUnix::LaunchDefaultWithFile(nsIFile* aFile) { + // if a default app is set, it means the application has been set from + // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to + // give the GNOME answer. + if (GetDefaultApplication()) { + return nsMIMEInfoImpl::LaunchDefaultWithFile(aFile); + } + + nsAutoCString nativePath; + aFile->GetNativePath(nativePath); + + nsCOMPtr giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return NS_ERROR_FAILURE; + } + + // nsGIOMimeApp->Launch wants a URI string instead of local file + nsresult rv; + nsCOMPtr ioservice = + do_GetService(NS_IOSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr uri; + rv = ioservice->NewFileURI(aFile, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr app; + if (NS_FAILED( + giovfs->GetAppForMimeType(mSchemeOrType, getter_AddRefs(app))) || + !app) { + return NS_ERROR_FILE_NOT_FOUND; + } + + return app->LaunchWithURI(uri, nullptr); +} diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.h b/uriloader/exthandler/unix/nsMIMEInfoUnix.h new file mode 100644 index 0000000000..2e32be4915 --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 3; 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 nsMIMEInfoUnix_h_ +#define nsMIMEInfoUnix_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoUnix : public nsMIMEInfoImpl { + public: + explicit nsMIMEInfoUnix(const char* aMIMEType = "") + : nsMIMEInfoImpl(aMIMEType) {} + explicit nsMIMEInfoUnix(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) {} + nsMIMEInfoUnix(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) {} + static bool HandlerExists(const char* aProtocolScheme); + + protected: + NS_IMETHOD GetHasDefaultHandler(bool* _retval) override; + + virtual nsresult LoadUriInternal(nsIURI* aURI) override; + + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) override; +}; + +#endif // nsMIMEInfoUnix_h_ diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.cpp b/uriloader/exthandler/unix/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..7f6eaa46f2 --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.cpp @@ -0,0 +1,1498 @@ +/* -*- Mode: C++; tab-width: 3; 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 +#include + +#include "nsOSHelperAppService.h" +#include "nsMIMEInfoUnix.h" +#ifdef MOZ_WIDGET_GTK +# include "nsGNOMERegistry.h" +# ifdef MOZ_BUILD_APP_IS_BROWSER +# include "nsIToolkitShellService.h" +# include "nsIGNOMEShellService.h" +# endif +#endif +#include "nsISupports.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsIFileStreams.h" +#include "nsILineInputStream.h" +#include "nsIFile.h" +#include "nsIProcess.h" +#include "nsNetCID.h" +#include "nsXPCOM.h" +#include "nsComponentManagerUtils.h" +#include "nsCRT.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsXULAppAPI.h" +#include "ContentHandlerService.h" +#include "prenv.h" // for PR_GetEnv() +#include "mozilla/EnumeratedRange.h" +#include "mozilla/Preferences.h" +#include "mozilla/ClearOnShutdown.h" +#include "nsMimeTypes.h" +#include + +using namespace mozilla; + +#define LOG(...) \ + MOZ_LOG(nsOSHelperAppService::sLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#define LOG_ENABLED() \ + MOZ_LOG_TEST(nsOSHelperAppService::sLog, mozilla::LogLevel::Debug) + +static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter, + const nsAString::const_iterator& aEnd_iter); +static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + const nsAString::const_iterator& aEnd_iter); + +inline bool IsNetscapeFormat(const nsACString& aBuffer); + +nsOSHelperAppService::~nsOSHelperAppService() {} + +/* + * Take a command with all the mailcap escapes in it and unescape it + * Ideally this needs the mime type, mime type options, and location of the + * temporary file, but this last can't be got from here + */ +// static +nsresult nsOSHelperAppService::UnescapeCommand(const nsAString& aEscapedCommand, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsACString& aUnEscapedCommand) { + LOG("-- UnescapeCommand"); + LOG("Command to escape: '%s'\n", + NS_LossyConvertUTF16toASCII(aEscapedCommand).get()); + // XXX This function will need to get the mime type and various stuff like + // that being passed in to work properly + + LOG( + ("UnescapeCommand really needs some work -- it should actually do some " + "unescaping\n")); + + CopyUTF16toUTF8(aEscapedCommand, aUnEscapedCommand); + LOG("Escaped command: '%s'\n", PromiseFlatCString(aUnEscapedCommand).get()); + return NS_OK; +} + +/* Put aSemicolon_iter at the first non-escaped semicolon after + * aStart_iter but before aEnd_iter + */ + +static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter, + const nsAString::const_iterator& aEnd_iter) { + bool semicolonFound = false; + while (aSemicolon_iter != aEnd_iter && !semicolonFound) { + switch (*aSemicolon_iter) { + case '\\': + aSemicolon_iter.advance(2); + break; + case ';': + semicolonFound = true; + break; + default: + ++aSemicolon_iter; + break; + } + } + return NS_OK; +} + +static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + const nsAString::const_iterator& aEnd_iter) { + nsAString::const_iterator iter(aStart_iter); + + // skip leading whitespace + while (iter != aEnd_iter && nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMajorTypeStart = iter; + + // find major/minor separator ('/') + while (iter != aEnd_iter && *iter != '/') { + ++iter; + } + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMajorTypeEnd = iter; + + // skip '/' + ++iter; + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMinorTypeStart = iter; + + // find end of minor type, delimited by whitespace or ';' + while (iter != aEnd_iter && !nsCRT::IsAsciiSpace(*iter) && *iter != ';') { + ++iter; + } + + aMinorTypeEnd = iter; + + return NS_OK; +} + +// TODO: We should consider not only caching the file location but maybe the +// file contents too? +enum class FileKind { + PrivateMimeTypes = 0, + GlobalMimeTypes, + PrivateMailCap, + GlobalMailCap, + + Count, +}; + +struct FileLocationCache { + struct Entry { + bool mIsCached = false; + nsresult mResult = NS_OK; + nsString mLocation; + + void Clear() { *this = {}; } + }; + + EnumeratedArray mEntries; + + static const char* PrefFor(FileKind aKind) { + switch (aKind) { + case FileKind::GlobalMimeTypes: + return "helpers.global_mime_types_file"; + case FileKind::PrivateMimeTypes: + return "helpers.private_mime_types_file"; + case FileKind::GlobalMailCap: + return "helpers.global_mailcap_file"; + case FileKind::PrivateMailCap: + return "helpers.private_mailcap_file"; + case FileKind::Count: + break; + } + MOZ_ASSERT_UNREACHABLE("Unknown file kind"); + return ""; + } + + void Clear() { + for (auto& entry : mEntries) { + entry.Clear(); + } + } + + static void PrefChangeCallback(const char*, void*) { Get().Clear(); } + + static FileLocationCache& Get() { + static FileLocationCache sCache; + static std::once_flag flag; + std::call_once(flag, [&] { + for (auto kind : + MakeEnumeratedRange(FileKind::PrivateMimeTypes, FileKind::Count)) { + Preferences::RegisterCallback(PrefChangeCallback, + nsDependentCString(PrefFor(kind))); + } + RunOnShutdown([] { + sCache.Clear(); + for (auto kind : + MakeEnumeratedRange(FileKind::PrivateMimeTypes, FileKind::Count)) { + Preferences::UnregisterCallback(PrefChangeCallback, + nsDependentCString(PrefFor(kind))); + } + }); + }); + return sCache; + } + + Entry& EntryFor(FileKind aKind) { return mEntries[aKind]; } +}; + +// The lookup order is: +// 1) user pref +// 2) env var (if any) +// 3) pref +static nsresult DoGetFileLocation(FileKind aKind, nsAString& aFileLocation) { + LOG("-- GetFileLocation(%d)\n", int(aKind)); + + aFileLocation.Truncate(); + + const char* envVar = [&]() -> const char* { + switch (aKind) { + case FileKind::PrivateMailCap: + return "PERSONAL_MAILCAP"; + case FileKind::GlobalMailCap: + return "MAILCAP"; + default: + return nullptr; + } + }(); + + const char* prefName = FileLocationCache::PrefFor(aKind); + + if (envVar) { + // If we have an env var we should check whether the pref is a user pref. If + // we do not, we don't care. + if (Preferences::HasUserValue(prefName) && + NS_SUCCEEDED(Preferences::GetString(prefName, aFileLocation))) { + return NS_OK; + } + + char* envValue = PR_GetEnv(envVar); + if (envValue && *envValue) { + // the pref is in the system charset and it's a filepath... The + // natural way to do the charset conversion is by just initing + // an nsIFile with the native path and asking it for the Unicode + // version. + nsresult rv; + nsCOMPtr file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->InitWithNativePath(nsDependentCString(envValue)); + NS_ENSURE_SUCCESS(rv, rv); + + return file->GetPath(aFileLocation); + } + } + + return Preferences::GetString(prefName, aFileLocation); +} + +static nsresult GetFileLocation(FileKind aKind, nsAString& aFileLocation) { + MOZ_ASSERT(NS_IsMainThread()); + auto& entry = FileLocationCache::Get().EntryFor(aKind); + if (!entry.mIsCached) { + entry.mIsCached = true; + entry.mResult = DoGetFileLocation(aKind, entry.mLocation); + } + aFileLocation = entry.mLocation; + return entry.mResult; +} + +/* Get the mime.types file names from prefs and look up info in them + based on extension */ +// static +nsresult nsOSHelperAppService::LookUpTypeAndDescription( + const nsAString& aFileExtension, nsAString& aMajorType, + nsAString& aMinorType, nsAString& aDescription, bool aUserData) { + LOG("-- LookUpTypeAndDescription for extension '%s'\n", + NS_LossyConvertUTF16toASCII(aFileExtension).get()); + nsAutoString mimeFileName; + + auto kind = + aUserData ? FileKind::PrivateMimeTypes : FileKind::GlobalMimeTypes; + nsresult rv = GetFileLocation(kind, mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetTypeAndDescriptionFromMimetypesFile( + mimeFileName, aFileExtension, aMajorType, aMinorType, aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + return rv; +} + +inline bool IsNetscapeFormat(const nsACString& aBuffer) { + return StringBeginsWith( + aBuffer, + nsLiteralCString( + "#--Netscape Communications Corporation MIME Information")) || + StringBeginsWith(aBuffer, "#--MCOM MIME Information"_ns); +} + +/* + * Create a file stream and line input stream for the filename. + * Leaves the first line of the file in aBuffer and sets the format to + * true for netscape files and false for normail ones + */ +// static +nsresult nsOSHelperAppService::CreateInputStream( + const nsAString& aFilename, nsIFileInputStream** aFileInputStream, + nsILineInputStream** aLineInputStream, nsACString& aBuffer, + bool* aNetscapeFormat, bool* aMore) { + LOG("-- CreateInputStream"); + nsresult rv = NS_OK; + + nsCOMPtr file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = file->InitWithPath(aFilename); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr fileStream( + do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = fileStream->Init(file, -1, -1, false); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr lineStream(do_QueryInterface(fileStream, &rv)); + + if (NS_FAILED(rv)) { + LOG("Interface trouble in stream land!"); + return rv; + } + + rv = lineStream->ReadLine(aBuffer, aMore); + if (NS_FAILED(rv)) { + fileStream->Close(); + return rv; + } + + *aNetscapeFormat = IsNetscapeFormat(aBuffer); + + *aFileInputStream = fileStream; + NS_ADDREF(*aFileInputStream); + *aLineInputStream = lineStream; + NS_ADDREF(*aLineInputStream); + + return NS_OK; +} + +/* Open the file, read the first line, decide what type of file it is, + then get info based on extension */ +// static +nsresult nsOSHelperAppService::GetTypeAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aFileExtension, + nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription) { + LOG("-- GetTypeAndDescriptionFromMimetypesFile\n"); + LOG("Getting type and description from types file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get()); + LOG("Using extension '%s'\n", + NS_LossyConvertUTF16toASCII(aFileExtension).get()); + nsCOMPtr mimeFile; + nsCOMPtr mimeTypes; + bool netscapeFormat; + nsAutoString buf; + nsAutoCString cBuf; + bool more = false; + nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), + getter_AddRefs(mimeTypes), cBuf, + &netscapeFormat, &more); + + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString extensions; + nsAutoStringN<101> entry; + nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, descriptionStart, descriptionEnd; + + do { + CopyASCIItoUTF16(cBuf, buf); + // read through, building up an entry. If we finish an entry, check for + // a match and return out of the loop if we match + + // skip comments and empty lines + if (!buf.IsEmpty() && buf.First() != '#') { + entry.Append(buf); + if (entry.Last() == '\\') { + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t( + ' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry + LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get()); + if (netscapeFormat) { + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + if (NS_FAILED(rv)) { + // We sometimes get things like RealPlayer appending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 106381. + LOG("Bogus entry; trying 'normal' mode\n"); + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } else { + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + if (NS_FAILED(rv)) { + // We sometimes get things like StarOffice prepending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 136670. + LOG("Bogus entry; trying 'Netscape' mode\n"); + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } + + if (NS_SUCCEEDED(rv)) { // entry parses + nsAString::const_iterator start, end; + extensions.BeginReading(start); + extensions.EndReading(end); + nsAString::const_iterator iter(start); + + while (start != end) { + FindCharInReadable(',', iter, end); + if (Substring(start, iter) + .Equals(aFileExtension, + nsCaseInsensitiveStringComparator)) { + // it's a match. Assign the type and description and run + aMajorType.Assign(Substring(majorTypeStart, majorTypeEnd)); + aMinorType.Assign(Substring(minorTypeStart, minorTypeEnd)); + aDescription.Assign(Substring(descriptionStart, descriptionEnd)); + mimeFile->Close(); + return NS_OK; + } + if (iter != end) { + ++iter; + } + start = iter; + } + } else { + LOG("Failed to parse entry: %s\n", + NS_LossyConvertUTF16toASCII(entry).get()); + } + // truncate the entry for the next iteration + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + // read the next line + rv = mimeTypes->ReadLine(cBuf, &more); + } while (NS_SUCCEEDED(rv)); + + mimeFile->Close(); + return rv; +} + +/* Get the mime.types file names from prefs and look up info in them + based on mimetype */ +// static +nsresult nsOSHelperAppService::LookUpExtensionsAndDescription( + const nsAString& aMajorType, const nsAString& aMinorType, + nsAString& aFileExtensions, nsAString& aDescription) { + LOG("-- LookUpExtensionsAndDescription for type '%s/%s'\n", + NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get()); + nsAutoString mimeFileName; + + nsresult rv = GetFileLocation(FileKind::PrivateMimeTypes, mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetExtensionsAndDescriptionFromMimetypesFile( + mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + if (NS_FAILED(rv) || aFileExtensions.IsEmpty()) { + rv = GetFileLocation(FileKind::GlobalMimeTypes, mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetExtensionsAndDescriptionFromMimetypesFile( + mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + } + return rv; +} + +/* Open the file, read the first line, decide what type of file it is, + then get info based on extension */ +// static +nsresult nsOSHelperAppService::GetExtensionsAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aFileExtensions, + nsAString& aDescription) { + LOG("-- GetExtensionsAndDescriptionFromMimetypesFile\n"); + LOG("Getting extensions and description from types file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get()); + LOG("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get()); + nsCOMPtr mimeFile; + nsCOMPtr mimeTypes; + bool netscapeFormat; + nsAutoCString cBuf; + nsAutoString buf; + bool more = false; + nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), + getter_AddRefs(mimeTypes), cBuf, + &netscapeFormat, &more); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString extensions; + nsAutoStringN<101> entry; + nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, descriptionStart, descriptionEnd; + + do { + CopyASCIItoUTF16(cBuf, buf); + // read through, building up an entry. If we finish an entry, check for + // a match and return out of the loop if we match + + // skip comments and empty lines + if (!buf.IsEmpty() && buf.First() != '#') { + entry.Append(buf); + if (entry.Last() == '\\') { + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t( + ' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry + LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get()); + if (netscapeFormat) { + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + + if (NS_FAILED(rv)) { + // We sometimes get things like RealPlayer appending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 106381. + LOG("Bogus entry; trying 'normal' mode\n"); + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } else { + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + + if (NS_FAILED(rv)) { + // We sometimes get things like StarOffice prepending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 136670. + LOG("Bogus entry; trying 'Netscape' mode\n"); + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } + + if (NS_SUCCEEDED(rv) && + Substring(majorTypeStart, majorTypeEnd) + .Equals(aMajorType, nsCaseInsensitiveStringComparator) && + Substring(minorTypeStart, minorTypeEnd) + .Equals(aMinorType, nsCaseInsensitiveStringComparator)) { + // it's a match + aFileExtensions.Assign(extensions); + aDescription.Assign(Substring(descriptionStart, descriptionEnd)); + mimeFile->Close(); + return NS_OK; + } + if (NS_FAILED(rv)) { + LOG("Failed to parse entry: %s\n", + NS_LossyConvertUTF16toASCII(entry).get()); + } + + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + // read the next line + rv = mimeTypes->ReadLine(cBuf, &more); + } while (NS_SUCCEEDED(rv)); + + mimeFile->Close(); + return rv; +} + +/* + * This parses a Netscape format mime.types entry. There are two + * possible formats: + * + * type=foo/bar; options exts="baz" description="Some type" + * + * and + * + * type=foo/bar; options description="Some type" exts="baz" + */ +// static +nsresult nsOSHelperAppService::ParseNetscapeMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd) { + LOG("-- ParseNetscapeMIMETypesEntry\n"); + NS_ASSERTION(!aEntry.IsEmpty(), + "Empty Netscape MIME types entry being parsed."); + + nsAString::const_iterator start_iter, end_iter, match_start, match_end; + + aEntry.BeginReading(start_iter); + aEntry.EndReading(end_iter); + + // skip trailing whitespace + do { + --end_iter; + } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter)); + // if we're pointing to a quote, don't advance -- we don't want to + // include the quote.... + if (*end_iter != '"') ++end_iter; + match_start = start_iter; + match_end = end_iter; + + // Get the major and minor types + // First the major type + if (!FindInReadable(u"type="_ns, match_start, match_end)) { + return NS_ERROR_FAILURE; + } + + match_start = match_end; + + while (match_end != end_iter && *match_end != '/') { + ++match_end; + } + if (match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + aMajorTypeStart = match_start; + aMajorTypeEnd = match_end; + + // now the minor type + if (++match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + match_start = match_end; + + while (match_end != end_iter && !nsCRT::IsAsciiSpace(*match_end) && + *match_end != ';') { + ++match_end; + } + if (match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + aMinorTypeStart = match_start; + aMinorTypeEnd = match_end; + + // ignore everything up to the end of the mime type from here on + start_iter = match_end; + + // get the extensions + match_start = match_end; + match_end = end_iter; + if (FindInReadable(u"exts="_ns, match_start, match_end)) { + nsAString::const_iterator extStart, extEnd; + + if (match_end == end_iter || + (*match_end == '"' && ++match_end == end_iter)) { + return NS_ERROR_FAILURE; + } + + extStart = match_end; + match_start = extStart; + match_end = end_iter; + if (FindInReadable(u"desc=\""_ns, match_start, match_end)) { + // exts= before desc=, so we have to find the actual end of the extensions + extEnd = match_start; + if (extEnd == extStart) { + return NS_ERROR_FAILURE; + } + + do { + --extEnd; + } while (extEnd != extStart && nsCRT::IsAsciiSpace(*extEnd)); + + if (extEnd != extStart && *extEnd == '"') { + --extEnd; + } + } else { + // desc= before exts=, so we can use end_iter as the end of the extensions + extEnd = end_iter; + } + aExtensions = Substring(extStart, extEnd); + } else { + // no extensions + aExtensions.Truncate(); + } + + // get the description + match_start = start_iter; + match_end = end_iter; + if (FindInReadable(u"desc=\""_ns, match_start, match_end)) { + aDescriptionStart = match_end; + match_start = aDescriptionStart; + match_end = end_iter; + if (FindInReadable(u"exts="_ns, match_start, match_end)) { + // exts= after desc=, so have to find actual end of description + aDescriptionEnd = match_start; + if (aDescriptionEnd == aDescriptionStart) { + return NS_ERROR_FAILURE; + } + + do { + --aDescriptionEnd; + } while (aDescriptionEnd != aDescriptionStart && + nsCRT::IsAsciiSpace(*aDescriptionEnd)); + } else { + // desc= after exts=, so use end_iter for the description end + aDescriptionEnd = end_iter; + } + } else { + // no description + aDescriptionStart = start_iter; + aDescriptionEnd = start_iter; + } + + return NS_OK; +} + +/* + * This parses a normal format mime.types entry. The format is: + * + * major/minor ext1 ext2 ext3 + */ +// static +nsresult nsOSHelperAppService::ParseNormalMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd) { + LOG("-- ParseNormalMIMETypesEntry\n"); + NS_ASSERTION(!aEntry.IsEmpty(), + "Empty Normal MIME types entry being parsed."); + + nsAString::const_iterator start_iter, end_iter, iter; + + aEntry.BeginReading(start_iter); + aEntry.EndReading(end_iter); + + // no description + aDescriptionStart = start_iter; + aDescriptionEnd = start_iter; + + // skip leading whitespace + while (start_iter != end_iter && nsCRT::IsAsciiSpace(*start_iter)) { + ++start_iter; + } + if (start_iter == end_iter) { + return NS_ERROR_FAILURE; + } + // skip trailing whitespace + do { + --end_iter; + } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter)); + + ++end_iter; // point to first whitespace char (or to end of string) + iter = start_iter; + + // get the major type + if (!FindCharInReadable('/', iter, end_iter)) return NS_ERROR_FAILURE; + + nsAString::const_iterator equals_sign_iter(start_iter); + if (FindCharInReadable('=', equals_sign_iter, iter)) + return NS_ERROR_FAILURE; // see bug 136670 + + aMajorTypeStart = start_iter; + aMajorTypeEnd = iter; + + // get the minor type + if (++iter == end_iter) { + return NS_ERROR_FAILURE; + } + start_iter = iter; + + while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + aMinorTypeStart = start_iter; + aMinorTypeEnd = iter; + + // get the extensions + aExtensions.Truncate(); + while (iter != end_iter) { + while (iter != end_iter && nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + + start_iter = iter; + while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + aExtensions.Append(Substring(start_iter, iter)); + if (iter != end_iter) { // not the last extension + aExtensions.Append(char16_t(',')); + } + } + + return NS_OK; +} + +// static +nsresult nsOSHelperAppService::LookUpHandlerAndDescription( + const nsAString& aMajorType, const nsAString& aMinorType, + nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags) { + // The mailcap lookup is two-pass to handle the case of mailcap files + // that have something like: + // + // text/*; emacs %s + // text/rtf; soffice %s + // + // in that order. We want to pick up "soffice" for text/rtf in such cases + nsresult rv = DoLookUpHandlerAndDescription( + aMajorType, aMinorType, aHandler, aDescription, aMozillaFlags, true); + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, aMinorType, aHandler, + aDescription, aMozillaFlags, false); + } + + // maybe we have an entry for "aMajorType/*"? + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler, + aDescription, aMozillaFlags, true); + } + + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler, + aDescription, aMozillaFlags, false); + } + + return rv; +} + +// static +nsresult nsOSHelperAppService::DoLookUpHandlerAndDescription( + const nsAString& aMajorType, const nsAString& aMinorType, + nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags, + bool aUserData) { + LOG("-- LookUpHandlerAndDescription for type '%s/%s'\n", + NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get()); + nsAutoString mailcapFileName; + + const auto kind = + aUserData ? FileKind::PrivateMailCap : FileKind::GlobalMailCap; + nsresult rv = GetFileLocation(kind, mailcapFileName); + if (NS_FAILED(rv) || mailcapFileName.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + return GetHandlerAndDescriptionFromMailcapFile(mailcapFileName, aMajorType, + aMinorType, aHandler, + aDescription, aMozillaFlags); +} + +// static +nsresult nsOSHelperAppService::GetHandlerAndDescriptionFromMailcapFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription, + nsAString& aMozillaFlags) { + LOG("-- GetHandlerAndDescriptionFromMailcapFile\n"); + LOG("Getting handler and description from mailcap file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get()); + LOG("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get()); + + nsresult rv = NS_OK; + bool more = false; + + nsCOMPtr file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = file->InitWithPath(aFilename); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr mailcapFile( + do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = mailcapFile->Init(file, -1, -1, false); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr mailcap(do_QueryInterface(mailcapFile, &rv)); + + if (NS_FAILED(rv)) { + LOG("Interface trouble in stream land!"); + return rv; + } + + nsAutoStringN<129> entry; + nsAutoStringN<81> buffer; + nsAutoCStringN<81> cBuffer; + rv = mailcap->ReadLine(cBuffer, &more); + if (NS_FAILED(rv)) { + mailcapFile->Close(); + return rv; + } + + do { // return on end-of-file in the loop + + CopyASCIItoUTF16(cBuffer, buffer); + if (!buffer.IsEmpty() && buffer.First() != '#') { + entry.Append(buffer); + if (entry.Last() == '\\') { // entry continues on next line + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t( + ' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry in entry. Check it for the type + LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get()); + + nsAString::const_iterator semicolon_iter, start_iter, end_iter, + majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd; + entry.BeginReading(start_iter); + entry.EndReading(end_iter); + semicolon_iter = start_iter; + FindSemicolon(semicolon_iter, end_iter); + if (semicolon_iter != + end_iter) { // we have something resembling a valid entry + rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, semicolon_iter); + if (NS_SUCCEEDED(rv) && + Substring(majorTypeStart, majorTypeEnd) + .Equals(aMajorType, nsCaseInsensitiveStringComparator) && + Substring(minorTypeStart, minorTypeEnd) + .Equals(aMinorType, nsCaseInsensitiveStringComparator)) { + // we have a match + bool match = true; + ++semicolon_iter; // point at the first char past the semicolon + start_iter = semicolon_iter; // handler string starts here + FindSemicolon(semicolon_iter, end_iter); + while (start_iter != semicolon_iter && + nsCRT::IsAsciiSpace(*start_iter)) { + ++start_iter; + } + + LOG("The real handler is: '%s'\n", + NS_LossyConvertUTF16toASCII( + Substring(start_iter, semicolon_iter)) + .get()); + + // XXX ugly hack. Just grab the executable name + nsAString::const_iterator end_handler_iter = semicolon_iter; + nsAString::const_iterator end_executable_iter = start_iter; + while (end_executable_iter != end_handler_iter && + !nsCRT::IsAsciiSpace(*end_executable_iter)) { + ++end_executable_iter; + } + // XXX End ugly hack + + aHandler = Substring(start_iter, end_executable_iter); + + nsAString::const_iterator start_option_iter, end_optionname_iter, + equal_sign_iter; + bool equalSignFound; + while (match && semicolon_iter != end_iter && + ++semicolon_iter != + end_iter) { // there are options left and we still match + start_option_iter = semicolon_iter; + // skip over leading whitespace + while (start_option_iter != end_iter && + nsCRT::IsAsciiSpace(*start_option_iter)) { + ++start_option_iter; + } + if (start_option_iter == end_iter) { // nothing actually here + break; + } + semicolon_iter = start_option_iter; + FindSemicolon(semicolon_iter, end_iter); + equal_sign_iter = start_option_iter; + equalSignFound = false; + while (equal_sign_iter != semicolon_iter && !equalSignFound) { + switch (*equal_sign_iter) { + case '\\': + equal_sign_iter.advance(2); + break; + case '=': + equalSignFound = true; + break; + default: + ++equal_sign_iter; + break; + } + } + end_optionname_iter = start_option_iter; + // find end of option name + while (end_optionname_iter != equal_sign_iter && + !nsCRT::IsAsciiSpace(*end_optionname_iter)) { + ++end_optionname_iter; + } + nsDependentSubstring optionName(start_option_iter, + end_optionname_iter); + if (equalSignFound) { + // This is an option that has a name and value + if (optionName.EqualsLiteral("description")) { + aDescription = Substring(++equal_sign_iter, semicolon_iter); + } else if (optionName.EqualsLiteral("x-mozilla-flags")) { + aMozillaFlags = Substring(++equal_sign_iter, semicolon_iter); + } else if (optionName.EqualsLiteral("test")) { + nsAutoCString testCommand; + rv = UnescapeCommand( + Substring(++equal_sign_iter, semicolon_iter), aMajorType, + aMinorType, testCommand); + if (NS_FAILED(rv)) continue; + nsCOMPtr process = + do_CreateInstance(NS_PROCESS_CONTRACTID, &rv); + if (NS_FAILED(rv)) continue; + nsCOMPtr file( + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) continue; + rv = file->InitWithNativePath("/bin/sh"_ns); + if (NS_FAILED(rv)) continue; + rv = process->Init(file); + if (NS_FAILED(rv)) continue; + const char* args[] = {"-c", testCommand.get()}; + LOG("Running Test: %s\n", testCommand.get()); + rv = process->Run(true, args, 2); + if (NS_FAILED(rv)) continue; + int32_t exitValue; + rv = process->GetExitValue(&exitValue); + if (NS_FAILED(rv)) continue; + LOG("Exit code: %d\n", exitValue); + if (exitValue) { + match = false; + } + } + } else { + // This is an option that just has a name but no value (eg + // "copiousoutput") + if (optionName.EqualsLiteral("needsterminal")) { + match = false; + } + } + } + + if (match) { // we did not fail any test clauses; all is good + // get out of here + mailcapFile->Close(); + return NS_OK; + } + // pretend that this match never happened + aDescription.Truncate(); + aMozillaFlags.Truncate(); + aHandler.Truncate(); + } + } + // zero out the entry for the next cycle + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + rv = mailcap->ReadLine(cBuffer, &more); + } while (NS_SUCCEEDED(rv)); + mailcapFile->Close(); + return rv; +} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + nsresult rv = NS_OK; + + if (!XRE_IsContentProcess()) { +#ifdef MOZ_WIDGET_GTK + // Check the GNOME registry for a protocol handler + *aHandlerExists = nsGNOMERegistry::HandlerExists(aProtocolScheme); +#else + *aHandlerExists = false; +#endif + } else { + *aHandlerExists = false; + nsCOMPtr handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv) && handlerSvc) { + rv = handlerSvc->ExistsForProtocolOS(nsCString(aProtocolScheme), + aHandlerExists); + } + } + + return rv; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription( + const nsACString& aScheme, nsAString& _retval) { +#ifdef MOZ_WIDGET_GTK + nsGNOMERegistry::GetAppDescForScheme(aScheme, _retval); + return _retval.IsEmpty() ? NS_ERROR_NOT_AVAILABLE : NS_OK; +#else + return NS_ERROR_NOT_AVAILABLE; +#endif +} + +NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol( + const nsACString& aScheme, bool* _retval) { + *_retval = false; +#if defined(MOZ_BUILD_APP_IS_BROWSER) && defined(MOZ_WIDGET_GTK) + if (nsCOMPtr shell = + do_GetService(NS_TOOLKITSHELLSERVICE_CONTRACTID)) { + return shell->IsDefaultForScheme(aScheme, _retval); + } +#endif + return NS_OK; +} + +nsresult nsOSHelperAppService::GetFileTokenForPath( + const char16_t* platformAppPath, nsIFile** aFile) { + LOG("-- nsOSHelperAppService::GetFileTokenForPath: '%s'\n", + NS_LossyConvertUTF16toASCII(platformAppPath).get()); + if (!*platformAppPath) { // empty filename--return error + NS_WARNING("Empty filename passed in."); + return NS_ERROR_INVALID_ARG; + } + + // first check if the base class implementation finds anything + nsresult rv = + nsExternalHelperAppService::GetFileTokenForPath(platformAppPath, aFile); + if (NS_SUCCEEDED(rv)) return rv; + // If the reason for failure was that the file doesn't exist, return too + // (because it means the path was absolute, and so that we shouldn't search in + // the path) + if (rv == NS_ERROR_FILE_NOT_FOUND) return rv; + + // If we get here, we really should have a relative path. + NS_ASSERTION(*platformAppPath != char16_t('/'), "Unexpected absolute path"); + + nsCOMPtr localFile(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID)); + + if (!localFile) return NS_ERROR_NOT_INITIALIZED; + + bool exists = false; + // ugly hack. Walk the PATH variable... + char* unixpath = PR_GetEnv("PATH"); + nsAutoCString path(unixpath); + + const char* start_iter = path.BeginReading(start_iter); + const char* colon_iter = start_iter; + const char* end_iter = path.EndReading(end_iter); + + while (start_iter != end_iter && !exists) { + while (colon_iter != end_iter && *colon_iter != ':') { + ++colon_iter; + } + localFile->InitWithNativePath(Substring(start_iter, colon_iter)); + rv = localFile->AppendRelativePath(nsDependentString(platformAppPath)); + // Failing AppendRelativePath is a bad thing - it should basically always + // succeed given a relative path. Show a warning if it does fail. + // To prevent infinite loops when it does fail, return at this point. + NS_ENSURE_SUCCESS(rv, rv); + localFile->Exists(&exists); + if (!exists) { + if (colon_iter == end_iter) { + break; + } + ++colon_iter; + start_iter = colon_iter; + } + } + + if (exists) { + rv = NS_OK; + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + *aFile = localFile; + NS_IF_ADDREF(*aFile); + + return rv; +} + +already_AddRefed nsOSHelperAppService::GetFromExtension( + const nsCString& aFileExt) { + // if the extension is empty, return immediately + if (aFileExt.IsEmpty()) { + return nullptr; + } + + LOG("Here we do an extension lookup for '%s'\n", aFileExt.get()); + + nsAutoString majorType, minorType, mime_types_description, + mailcap_description, handler, mozillaFlags; + + nsresult rv = + LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType, + minorType, mime_types_description, true); + + if (NS_FAILED(rv) || majorType.IsEmpty()) { +#ifdef MOZ_WIDGET_GTK + LOG("Looking in GNOME registry\n"); + RefPtr gnomeInfo = + nsGNOMERegistry::GetFromExtension(aFileExt); + if (gnomeInfo) { + LOG("Got MIMEInfo from GNOME registry\n"); + return gnomeInfo.forget(); + } +#endif + + rv = LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType, + minorType, mime_types_description, false); + } + + if (NS_FAILED(rv)) { + return nullptr; + } + + NS_LossyConvertUTF16toASCII asciiMajorType(majorType); + NS_LossyConvertUTF16toASCII asciiMinorType(minorType); + + LOG("Type/Description results: majorType='%s', minorType='%s', " + "description='%s'\n", + asciiMajorType.get(), asciiMinorType.get(), + NS_LossyConvertUTF16toASCII(mime_types_description).get()); + + if (majorType.IsEmpty() && minorType.IsEmpty()) { + // we didn't get a type mapping, so we can't do anything useful + return nullptr; + } + + nsAutoCString mimeType(asciiMajorType + "/"_ns + asciiMinorType); + RefPtr mimeInfo = new nsMIMEInfoUnix(mimeType); + + mimeInfo->AppendExtension(aFileExt); + rv = LookUpHandlerAndDescription(majorType, minorType, handler, + mailcap_description, mozillaFlags); + LOG("Handler/Description results: handler='%s', description='%s', " + "mozillaFlags='%s'\n", + NS_LossyConvertUTF16toASCII(handler).get(), + NS_LossyConvertUTF16toASCII(mailcap_description).get(), + NS_LossyConvertUTF16toASCII(mozillaFlags).get()); + mailcap_description.Trim(" \t\""); + mozillaFlags.Trim(" \t"); + if (!mime_types_description.IsEmpty()) { + mimeInfo->SetDescription(mime_types_description); + } else { + mimeInfo->SetDescription(mailcap_description); + } + + if (NS_SUCCEEDED(rv) && handler.IsEmpty()) { + rv = NS_ERROR_NOT_AVAILABLE; + } + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr handlerFile; + rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile)); + + if (NS_SUCCEEDED(rv)) { + mimeInfo->SetDefaultApplication(handlerFile); + mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + mimeInfo->SetDefaultDescription(handler); + } + } + + if (NS_FAILED(rv)) { + mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } + + return mimeInfo.forget(); +} + +already_AddRefed nsOSHelperAppService::GetFromType( + const nsCString& aMIMEType) { + // if the type is empty, return immediately + if (aMIMEType.IsEmpty()) { + return nullptr; + } + + LOG("Here we do a mimetype lookup for '%s'\n", aMIMEType.get()); + + // extract the major and minor types + NS_ConvertASCIItoUTF16 mimeType(aMIMEType); + nsAString::const_iterator start_iter, end_iter, majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd; + + mimeType.BeginReading(start_iter); + mimeType.EndReading(end_iter); + + // XXX FIXME: add typeOptions parsing in here + nsresult rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, end_iter); + + if (NS_FAILED(rv)) { + return nullptr; + } + + nsDependentSubstring majorType(majorTypeStart, majorTypeEnd); + nsDependentSubstring minorType(minorTypeStart, minorTypeEnd); + + // First check the user's private mailcap file + nsAutoString mailcap_description, handler, mozillaFlags; + DoLookUpHandlerAndDescription(majorType, minorType, handler, + mailcap_description, mozillaFlags, true); + + LOG("Private Handler/Description results: handler='%s', description='%s'\n", + NS_LossyConvertUTF16toASCII(handler).get(), + NS_LossyConvertUTF16toASCII(mailcap_description).get()); + + // Now look up our extensions + nsAutoString extensions, mime_types_description; + LookUpExtensionsAndDescription(majorType, minorType, extensions, + mime_types_description); + +#ifdef MOZ_WIDGET_GTK + if (handler.IsEmpty()) { + RefPtr gnomeInfo = nsGNOMERegistry::GetFromType(aMIMEType); + if (gnomeInfo) { + LOG("Got MIMEInfo from GNOME registry without extensions; setting them " + "to %s\n", + NS_LossyConvertUTF16toASCII(extensions).get()); + + NS_ASSERTION(!gnomeInfo->HasExtensions(), "How'd that happen?"); + gnomeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions)); + return gnomeInfo.forget(); + } + } +#endif + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, minorType, handler, + mailcap_description, mozillaFlags, false); + } + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler, + mailcap_description, mozillaFlags, true); + } + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler, + mailcap_description, mozillaFlags, false); + } + + LOG("Handler/Description results: handler='%s', description='%s', " + "mozillaFlags='%s'\n", + NS_LossyConvertUTF16toASCII(handler).get(), + NS_LossyConvertUTF16toASCII(mailcap_description).get(), + NS_LossyConvertUTF16toASCII(mozillaFlags).get()); + + mailcap_description.Trim(" \t\""); + mozillaFlags.Trim(" \t"); + + if (handler.IsEmpty() && extensions.IsEmpty() && + mailcap_description.IsEmpty() && mime_types_description.IsEmpty()) { + // No real useful info + return nullptr; + } + + RefPtr mimeInfo = new nsMIMEInfoUnix(aMIMEType); + + mimeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions)); + if (!mime_types_description.IsEmpty()) { + mimeInfo->SetDescription(mime_types_description); + } else { + mimeInfo->SetDescription(mailcap_description); + } + + rv = NS_ERROR_NOT_AVAILABLE; + nsCOMPtr handlerFile; + if (!handler.IsEmpty()) { + rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile)); + } + + if (NS_SUCCEEDED(rv)) { + mimeInfo->SetDefaultApplication(handlerFile); + mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + mimeInfo->SetDefaultDescription(handler); + } else { + mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } + + return mimeInfo.forget(); +} + +nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aType, + const nsACString& aFileExt, + bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + *aFound = true; + RefPtr retval; + // Fallback to lookup by extension when generic 'application/octet-stream' + // content type is received. + if (!aType.EqualsLiteral(APPLICATION_OCTET_STREAM)) { + retval = GetFromType(PromiseFlatCString(aType)); + } + bool hasDefault = false; + if (retval) retval->GetHasDefaultHandler(&hasDefault); + if (!retval || !hasDefault) { + RefPtr miByExt = + GetFromExtension(PromiseFlatCString(aFileExt)); + // If we had no extension match, but a type match, use that + if (!miByExt && retval) { + retval.forget(aMIMEInfo); + return NS_OK; + } + // If we had an extension match but no type match, set the mimetype and use + // it + if (!retval && miByExt) { + if (!aType.IsEmpty()) miByExt->SetMIMEType(aType); + miByExt.swap(retval); + + retval.forget(aMIMEInfo); + return NS_OK; + } + // If we got nothing, make a new mimeinfo + if (!retval) { + *aFound = false; + retval = new nsMIMEInfoUnix(aType); + if (retval) { + if (!aFileExt.IsEmpty()) retval->AppendExtension(aFileExt); + } + + retval.forget(aMIMEInfo); + return NS_OK; + } + + // Copy the attributes of retval (mimeinfo from type) onto miByExt, to + // return it + // but reset to just collected mDefaultAppDescription (from ext) + nsAutoString byExtDefault; + miByExt->GetDefaultDescription(byExtDefault); + retval->SetDefaultDescription(byExtDefault); + retval->CopyBasicDataTo(miByExt); + + miByExt.swap(retval); + } + retval.forget(aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) { + NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); + + nsresult rv = + OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found); + if (NS_FAILED(rv)) return rv; + + nsMIMEInfoUnix* handlerInfo = + new nsMIMEInfoUnix(aScheme, nsMIMEInfoBase::eProtocolInfo); + NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(*_retval = handlerInfo); + + if (!*found) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + return NS_OK; + } + + nsAutoString desc; + GetApplicationDescription(aScheme, desc); + handlerInfo->SetDefaultDescription(desc); + + return NS_OK; +} diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.h b/uriloader/exthandler/unix/nsOSHelperAppService.h new file mode 100644 index 0000000000..25feeac359 --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.h @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 3; 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 nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is +// implemented on each platform. It contains platform specific code for finding +// helper applications for a given mime type in addition to launching those +// applications. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" + +class nsIFileInputStream; +class nsILineInputStream; +class nsMIMEInfoBase; + +class nsOSHelperAppService : public nsExternalHelperAppService { + public: + virtual ~nsOSHelperAppService(); + + // method overrides for mime.types and mime.info look up steps + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMimeType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) override; + + // override nsIExternalProtocolService methods + nsresult OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) override; + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got + // out of the rdf data source. This can be a mac file + // spec, a unix path or a windows path depending on the + // platform + // aFile --> an nsIFile representation of that platform application path. + virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile) override; + + protected: + already_AddRefed GetFromType(const nsCString& aMimeType); + already_AddRefed GetFromExtension(const nsCString& aFileExt); + + private: + // Helper methods which have to access static members + static nsresult UnescapeCommand(const nsAString& aEscapedCommand, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsACString& aUnEscapedCommand); + static nsresult LookUpTypeAndDescription(const nsAString& aFileExtension, + nsAString& aMajorType, + nsAString& aMinorType, + nsAString& aDescription, + bool aUserData); + static nsresult CreateInputStream(const nsAString& aFilename, + nsIFileInputStream** aFileInputStream, + nsILineInputStream** aLineInputStream, + nsACString& aBuffer, bool* aNetscapeFormat, + bool* aMore); + + static nsresult GetTypeAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aFileExtension, + nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription); + + static nsresult LookUpExtensionsAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aFileExtensions, + nsAString& aDescription); + + static nsresult GetExtensionsAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aFileExtensions, + nsAString& aDescription); + + static nsresult ParseNetscapeMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd); + + static nsresult ParseNormalMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd); + + static nsresult LookUpHandlerAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags); + + static nsresult DoLookUpHandlerAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags, + bool aUserData); + + static nsresult GetHandlerAndDescriptionFromMailcapFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription, + nsAString& aMozillaFlags); +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.cpp b/uriloader/exthandler/win/nsMIMEInfoWin.cpp new file mode 100644 index 0000000000..758b2018a7 --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.cpp @@ -0,0 +1,913 @@ +/* -*- Mode: C++; tab-width: 3; 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 "nsArrayEnumerator.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMArray.h" +#include "nsLocalFile.h" +#include "nsMIMEInfoWin.h" +#include "nsIMIMEService.h" +#include "nsNetUtil.h" +#include +#include +#include "nsIMutableArray.h" +#include "nsTArray.h" +#include +#include "nsIWindowsRegKey.h" +#include "nsUnicharUtils.h" +#include "nsTextToSubURI.h" +#include "nsVariant.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/ShellHeaderOnlyUtils.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/UrlmonHeaderOnlyUtils.h" +#include "mozilla/UniquePtrExtensions.h" + +#define RUNDLL32_EXE L"\\rundll32.exe" + +NS_IMPL_ISUPPORTS_INHERITED(nsMIMEInfoWin, nsMIMEInfoBase, nsIPropertyBag) + +nsMIMEInfoWin::~nsMIMEInfoWin() {} + +nsresult nsMIMEInfoWin::LaunchDefaultWithFile(nsIFile* aFile) { + // Launch the file, unless it is an executable. + bool executable = true; + aFile->IsExecutable(&executable); + if (executable) return NS_ERROR_FAILURE; + + return aFile->Launch(); +} + +nsresult nsMIMEInfoWin::ShellExecuteWithIFile(nsIFile* aExecutable, int aArgc, + const wchar_t** aArgv) { + nsresult rv; + + NS_ASSERTION(aArgc >= 1, "aArgc must be at least 1"); + + nsAutoString execPath; + rv = aExecutable->GetTarget(execPath); + if (NS_FAILED(rv) || execPath.IsEmpty()) { + rv = aExecutable->GetPath(execPath); + } + if (NS_FAILED(rv)) { + return rv; + } + + auto assembledArgs = mozilla::MakeCommandLine(aArgc, aArgv); + if (!assembledArgs) { + return NS_ERROR_FILE_EXECUTION_FAILED; + } + + _bstr_t execPathBStr(execPath.get()); + // Pass VT_ERROR/DISP_E_PARAMNOTFOUND to omit an optional RPC parameter + // to execute a file with the default verb. + _variant_t verbDefault(DISP_E_PARAMNOTFOUND, VT_ERROR); + _variant_t workingDir; + _variant_t showCmd(SW_SHOWNORMAL); + + // Ask Explorer to ShellExecute on our behalf, as some applications such as + // Skype for Business do not start correctly when inheriting our process's + // migitation policies. + // It does not work in a special environment such as Citrix. In such a case + // we fall back to launching an application as a child process. We need to + // find a way to handle the combination of these interop issues. + mozilla::LauncherVoidResult shellExecuteOk = mozilla::ShellExecuteByExplorer( + execPathBStr, assembledArgs.get(), verbDefault, workingDir, showCmd); + if (shellExecuteOk.isErr()) { + // No need to pass assembledArgs to LaunchWithIProcess. aArgv will be + // processed in nsProcess::RunProcess. + return LaunchWithIProcess(aExecutable, aArgc, + reinterpret_cast(aArgv)); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::LaunchWithFile(nsIFile* aFile) { + nsresult rv; + + // it doesn't make any sense to call this on protocol handlers + NS_ASSERTION(mClass == eMIMEInfo, + "nsMIMEInfoBase should have mClass == eMIMEInfo"); + + if (AutomationOnlyCheckIfLaunchStubbed(aFile)) { + return NS_OK; + } + + if (mPreferredAction == useSystemDefault) { + nsCOMPtr defaultApp = GetDefaultApplication(); + if (defaultApp && + mozilla::StaticPrefs::browser_pdf_launchDefaultEdgeAsApp()) { + // Since Edgium is the default handler for PDF and other kinds of files, + // if we're using the OS default and it's Edgium prefer its app mode so it + // operates as a viewer (without browser toolbars). Bug 1632277. + nsAutoCString defaultAppExecutable; + rv = defaultApp->GetNativeLeafName(defaultAppExecutable); + if (NS_SUCCEEDED(rv) && + defaultAppExecutable.LowerCaseEqualsLiteral("msedge.exe")) { + nsAutoString path; + rv = aFile->GetPath(path); + if (NS_SUCCEEDED(rv)) { + // If the --app flag doesn't work we'll want to fallback to a + // regular path. Send two args so we call `msedge.exe --app={path} + // {path}`. + nsAutoString appArg; + appArg.AppendLiteral("--app="); + appArg.Append(path); + const wchar_t* argv[] = {appArg.get(), path.get()}; + + return ShellExecuteWithIFile(defaultApp, mozilla::ArrayLength(argv), + argv); + } + } + } + return LaunchDefaultWithFile(aFile); + } + + if (mPreferredAction == useHelperApp) { + if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND; + + // at the moment, we only know how to hand files off to local handlers + nsCOMPtr localHandler = + do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr executable; + rv = localHandler->GetExecutable(getter_AddRefs(executable)); + NS_ENSURE_SUCCESS(rv, rv); + + // Deal with local dll based handlers + nsCString filename; + executable->GetNativeLeafName(filename); + if (filename.Length() > 4) { + nsCString extension(Substring(filename, filename.Length() - 4, 4)); + + if (extension.LowerCaseEqualsLiteral(".dll")) { + nsAutoString args; + + // executable is rundll32, everything else is a list of parameters, + // including the dll handler. + if (!GetDllLaunchInfo(executable, aFile, args, false)) + return NS_ERROR_INVALID_ARG; + + WCHAR rundll32Path[MAX_PATH + sizeof(RUNDLL32_EXE) / sizeof(WCHAR) + + 1] = {L'\0'}; + if (!GetSystemDirectoryW(rundll32Path, MAX_PATH)) { + return NS_ERROR_FILE_NOT_FOUND; + } + lstrcatW(rundll32Path, RUNDLL32_EXE); + + SHELLEXECUTEINFOW seinfo; + memset(&seinfo, 0, sizeof(seinfo)); + seinfo.cbSize = sizeof(SHELLEXECUTEINFOW); + seinfo.fMask = 0; + seinfo.hwnd = nullptr; + seinfo.lpVerb = nullptr; + seinfo.lpFile = rundll32Path; + seinfo.lpParameters = args.get(); + seinfo.lpDirectory = nullptr; + seinfo.nShow = SW_SHOWNORMAL; + if (ShellExecuteExW(&seinfo)) return NS_OK; + + switch ((LONG_PTR)seinfo.hInstApp) { + case 0: + case SE_ERR_OOM: + return NS_ERROR_OUT_OF_MEMORY; + case SE_ERR_ACCESSDENIED: + return NS_ERROR_FILE_ACCESS_DENIED; + case SE_ERR_ASSOCINCOMPLETE: + case SE_ERR_NOASSOC: + return NS_ERROR_UNEXPECTED; + case SE_ERR_DDEBUSY: + case SE_ERR_DDEFAIL: + case SE_ERR_DDETIMEOUT: + return NS_ERROR_NOT_AVAILABLE; + case SE_ERR_DLLNOTFOUND: + return NS_ERROR_FAILURE; + case SE_ERR_SHARE: + return NS_ERROR_FILE_IS_LOCKED; + default: + switch (GetLastError()) { + case ERROR_FILE_NOT_FOUND: + return NS_ERROR_FILE_NOT_FOUND; + case ERROR_PATH_NOT_FOUND: + return NS_ERROR_FILE_UNRECOGNIZED_PATH; + case ERROR_BAD_FORMAT: + return NS_ERROR_FILE_CORRUPTED; + } + } + return NS_ERROR_FILE_EXECUTION_FAILED; + } + } + nsAutoString path; + aFile->GetPath(path); + const wchar_t* argv[] = {path.get()}; + return ShellExecuteWithIFile(executable, mozilla::ArrayLength(argv), argv); + } + + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetHasDefaultHandler(bool* _retval) { + // We have a default application if we have a description + // We can ShellExecute anything; however, callers are probably interested if + // there is really an application associated with this type of file + *_retval = !mDefaultAppDescription.IsEmpty(); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetEnumerator(nsISimpleEnumerator** _retval) { + nsCOMArray properties; + + nsCOMPtr variant; + GetProperty(u"defaultApplicationIconURL"_ns, getter_AddRefs(variant)); + if (variant) properties.AppendObject(variant); + + GetProperty(u"customApplicationIconURL"_ns, getter_AddRefs(variant)); + if (variant) properties.AppendObject(variant); + + return NS_NewArrayEnumerator(_retval, properties, NS_GET_IID(nsIVariant)); +} + +static nsresult GetIconURLVariant(nsIFile* aApplication, nsIVariant** _retval) { + nsAutoCString fileURLSpec; + NS_GetURLSpecFromFile(aApplication, fileURLSpec); + nsAutoCString iconURLSpec; + iconURLSpec.AssignLiteral("moz-icon://"); + iconURLSpec += fileURLSpec; + RefPtr writable(new nsVariant()); + writable->SetAsAUTF8String(iconURLSpec); + writable.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetProperty(const nsAString& aName, nsIVariant** _retval) { + nsresult rv; + nsCOMPtr defaultApp = GetDefaultApplication(); + if (defaultApp && aName.EqualsLiteral(PROPERTY_DEFAULT_APP_ICON_URL)) { + rv = GetIconURLVariant(defaultApp, _retval); + NS_ENSURE_SUCCESS(rv, rv); + } else if (mPreferredApplication && + aName.EqualsLiteral(PROPERTY_CUSTOM_APP_ICON_URL)) { + nsCOMPtr localHandler = + do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr executable; + rv = localHandler->GetExecutable(getter_AddRefs(executable)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetIconURLVariant(executable, _retval); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// this implementation was pretty much copied verbatime from +// Tony Robinson's code in nsExternalProtocolWin.cpp +nsresult nsMIMEInfoWin::LoadUriInternal(nsIURI* aURL) { + nsresult rv = NS_OK; + + // 1. Find the default app for this protocol + // 2. Set up the command line + // 3. Launch the app. + + // For now, we'll just cheat essentially, check for the command line + // then just call ShellExecute()! + + if (aURL) { + // extract the url spec from the url + nsAutoCString urlSpec; + aURL->GetAsciiSpec(urlSpec); + + // Unescape non-ASCII characters in the URL + nsAutoString utf16Spec; + if (NS_FAILED(nsTextToSubURI::UnEscapeNonAsciiURI("UTF-8"_ns, urlSpec, + utf16Spec))) { + CopyASCIItoUTF16(urlSpec, utf16Spec); + } + + // Ask the shell/urlmon to parse |utf16Spec| to avoid malformed URLs. + // Failure is indicative of a potential security issue so we should + // bail out if so. + mozilla::LauncherResult<_bstr_t> validatedUri = + mozilla::UrlmonValidateUri(utf16Spec.get()); + if (validatedUri.isErr()) { + return NS_ERROR_FAILURE; + } + + _variant_t args; + _variant_t verb(L"open"); + _variant_t workingDir; + _variant_t showCmd(SW_SHOWNORMAL); + + // To open a uri, we first try ShellExecuteByExplorer, which starts a new + // process as a child process of explorer.exe, because applications may not + // support the mitigation policies inherited from our process. If it fails, + // we fall back to ShellExecuteExW. + // + // For Thunderbird, however, there is a known issue that + // ShellExecuteByExplorer succeeds but explorer.exe shows an error popup + // if a uri to open includes credentials. This does not happen in Firefox + // because Firefox does not have to launch a process to open a uri. + // + // Since Thunderbird does not use mitigation policies which could cause + // compatibility issues, we get no benefit from using + // ShellExecuteByExplorer. Thus we skip it and go straight to + // ShellExecuteExW for Thunderbird. +#ifndef MOZ_THUNDERBIRD + mozilla::LauncherVoidResult shellExecuteOk = + mozilla::ShellExecuteByExplorer(validatedUri.inspect(), args, verb, + workingDir, showCmd); + if (shellExecuteOk.isOk()) { + return NS_OK; + } +#endif // MOZ_THUNDERBIRD + + SHELLEXECUTEINFOW sinfo = {sizeof(sinfo)}; + sinfo.fMask = SEE_MASK_NOASYNC; + sinfo.lpVerb = V_BSTR(&verb); + sinfo.nShow = showCmd; + sinfo.lpFile = validatedUri.inspect(); + + BOOL result = ShellExecuteExW(&sinfo); + if (!result || reinterpret_cast(sinfo.hInstApp) < 32) { + rv = NS_ERROR_FAILURE; + } + } + + return rv; +} + +void nsMIMEInfoWin::UpdateDefaultInfoIfStale() { + if (!mIsDefaultAppInfoFresh) { + nsCOMPtr mime = do_GetService("@mozilla.org/mime;1"); + if (mime) { + mime->UpdateDefaultAppInfo(static_cast(this)); + } + mIsDefaultAppInfoFresh = true; + } +} + +// Given a path to a local file, return its nsILocalHandlerApp instance. +bool nsMIMEInfoWin::GetLocalHandlerApp(const nsAString& aCommandHandler, + nsCOMPtr& aApp) { + nsCOMPtr locfile; + nsresult rv = NS_NewLocalFile(aCommandHandler, true, getter_AddRefs(locfile)); + if (NS_FAILED(rv)) return false; + + aApp = do_CreateInstance("@mozilla.org/uriloader/local-handler-app;1"); + if (!aApp) return false; + + aApp->SetExecutable(locfile); + return true; +} + +// Return the cleaned up file path associated with a command verb +// located in root/Applications. +bool nsMIMEInfoWin::GetAppsVerbCommandHandler(const nsAString& appExeName, + nsAString& applicationPath, + bool edit) { + nsCOMPtr appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return false; + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe + nsAutoString applicationsPath; + applicationsPath.AppendLiteral("Applications\\"); + applicationsPath.Append(appExeName); + + nsresult rv = + appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + // Check for the NoOpenWith flag, if it exists + uint32_t value; + if (NS_SUCCEEDED(appKey->ReadIntValue(u"NoOpenWith"_ns, &value)) && + value == 1) + return false; + + nsAutoString dummy; + if (NS_SUCCEEDED(appKey->ReadStringValue(u"NoOpenWith"_ns, dummy))) + return false; + + appKey->Close(); + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe\shell\open\command + applicationsPath.AssignLiteral("Applications\\"); + applicationsPath.Append(appExeName); + if (!edit) + applicationsPath.AppendLiteral("\\shell\\open\\command"); + else + applicationsPath.AppendLiteral("\\shell\\edit\\command"); + + rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) { + // Expand environment vars, clean up any misc. + if (!nsLocalFile::CleanupCmdHandlerPath(appFilesystemCommand)) return false; + + applicationPath = appFilesystemCommand; + return true; + } + return false; +} + +// Return a fully populated command string based on +// passing information. Used in launchWithFile to trace +// back to the full handler path based on the dll. +// (dll, targetfile, return args, open/edit) +bool nsMIMEInfoWin::GetDllLaunchInfo(nsIFile* aDll, nsIFile* aFile, + nsAString& args, bool edit) { + if (!aDll || !aFile) return false; + + nsString appExeName; + aDll->GetLeafName(appExeName); + + nsCOMPtr appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return false; + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe + nsAutoString applicationsPath; + applicationsPath.AppendLiteral("Applications\\"); + applicationsPath.Append(appExeName); + + nsresult rv = + appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + // Check for the NoOpenWith flag, if it exists + uint32_t value; + rv = appKey->ReadIntValue(u"NoOpenWith"_ns, &value); + if (NS_SUCCEEDED(rv) && value == 1) return false; + + nsAutoString dummy; + if (NS_SUCCEEDED(appKey->ReadStringValue(u"NoOpenWith"_ns, dummy))) + return false; + + appKey->Close(); + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe\shell\open\command + applicationsPath.AssignLiteral("Applications\\"); + applicationsPath.Append(appExeName); + if (!edit) + applicationsPath.AppendLiteral("\\shell\\open\\command"); + else + applicationsPath.AppendLiteral("\\shell\\edit\\command"); + + rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) { + // Replace embedded environment variables. + uint32_t bufLength = + ::ExpandEnvironmentStringsW(appFilesystemCommand.get(), nullptr, 0); + if (bufLength == 0) // Error + return false; + + auto destination = mozilla::MakeUniqueFallible(bufLength); + if (!destination) return false; + if (!::ExpandEnvironmentStringsW(appFilesystemCommand.get(), + destination.get(), bufLength)) + return false; + + appFilesystemCommand.Assign(destination.get()); + + // C:\Windows\System32\rundll32.exe "C:\Program Files\Windows + // Photo Gallery\PhotoViewer.dll", ImageView_Fullscreen %1 + nsAutoString params; + constexpr auto rundllSegment = u"rundll32.exe "_ns; + int32_t index = appFilesystemCommand.Find(rundllSegment); + if (index > kNotFound) { + params.Append( + Substring(appFilesystemCommand, index + rundllSegment.Length())); + } else { + params.Append(appFilesystemCommand); + } + + // check to make sure we have a %1 and fill it + constexpr auto percentOneParam = u"%1"_ns; + index = params.Find(percentOneParam); + if (index == kNotFound) // no parameter + return false; + + nsString target; + aFile->GetTarget(target); + params.Replace(index, 2, target); + + args = params; + + return true; + } + return false; +} + +// Return the cleaned up file path associated with a progid command +// verb located in root. +bool nsMIMEInfoWin::GetProgIDVerbCommandHandler(const nsAString& appProgIDName, + nsAString& applicationPath, + bool edit) { + nsCOMPtr appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return false; + + nsAutoString appProgId(appProgIDName); + + // HKEY_CLASSES_ROOT\Windows.XPSReachViewer\shell\open\command + if (!edit) + appProgId.AppendLiteral("\\shell\\open\\command"); + else + appProgId.AppendLiteral("\\shell\\edit\\command"); + + nsresult rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, appProgId, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) { + // Expand environment vars, clean up any misc. + if (!nsLocalFile::CleanupCmdHandlerPath(appFilesystemCommand)) return false; + + applicationPath = appFilesystemCommand; + return true; + } + return false; +} + +// Helper routine used in tracking app lists. Converts path +// entries to lower case and stores them in the trackList array. +void nsMIMEInfoWin::ProcessPath(nsCOMPtr& appList, + nsTArray& trackList, + const nsAString& appFilesystemCommand) { + nsAutoString lower(appFilesystemCommand); + ToLowerCase(lower); + + // Don't include firefox.exe in the list + WCHAR exe[MAX_PATH + 1]; + uint32_t len = GetModuleFileNameW(nullptr, exe, MAX_PATH); + if (len < MAX_PATH && len != 0) { + int32_t index = lower.Find(reinterpret_cast(exe)); + if (index != -1) return; + } + + nsCOMPtr aApp; + if (!GetLocalHandlerApp(appFilesystemCommand, aApp)) return; + + // Save in our main tracking arrays + appList->AppendElement(aApp); + trackList.AppendElement(lower); +} + +// Helper routine that handles a compare between a path +// and an array of paths. +static bool IsPathInList(nsAString& appPath, nsTArray& trackList) { + // trackList data is always lowercase, see ProcessPath + // above. + nsAutoString tmp(appPath); + ToLowerCase(tmp); + + for (uint32_t i = 0; i < trackList.Length(); i++) { + if (tmp.Equals(trackList[i])) return true; + } + return false; +} + +/** + * Returns a list of nsILocalHandlerApp objects containing local + * handlers associated with this mimeinfo. Implemented per + * platform using information in this object to generate the + * best list. Typically used for an "open with" style user + * option. + * + * @return nsIArray of nsILocalHandlerApp + */ +NS_IMETHODIMP +nsMIMEInfoWin::GetPossibleLocalHandlers(nsIArray** _retval) { + nsresult rv; + + *_retval = nullptr; + + nsCOMPtr appList = do_CreateInstance("@mozilla.org/array;1"); + + if (!appList) return NS_ERROR_FAILURE; + + nsTArray trackList; + + nsAutoCString fileExt; + GetPrimaryExtension(fileExt); + + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_FAILURE; + nsCOMPtr appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return NS_ERROR_FAILURE; + + nsAutoString workingRegistryPath; + + bool extKnown = false; + if (fileExt.IsEmpty()) { + extKnown = true; + // Mime type discovery is possible in some cases, through + // HKEY_CLASSES_ROOT\MIME\Database\Content Type, however, a number + // of file extensions related to mime type are simply not defined, + // (application/rss+xml & application/atom+xml are good examples) + // in which case we can only provide a generic list. + nsAutoCString mimeType; + GetMIMEType(mimeType); + if (!mimeType.IsEmpty()) { + workingRegistryPath.AppendLiteral("MIME\\Database\\Content Type\\"); + workingRegistryPath.Append(NS_ConvertASCIItoUTF16(mimeType)); + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString mimeFileExt; + if (NS_SUCCEEDED(regKey->ReadStringValue(u""_ns, mimeFileExt))) { + CopyUTF16toUTF8(mimeFileExt, fileExt); + extKnown = false; + } + } + } + } + + nsAutoString fileExtToUse; + if (!fileExt.IsEmpty() && fileExt.First() != '.') { + fileExtToUse = char16_t('.'); + } + fileExtToUse.Append(NS_ConvertUTF8toUTF16(fileExt)); + + // Note, the order in which these occur has an effect on the + // validity of the resulting display list. + + if (!extKnown) { + // 1) Get the default handler if it exists + workingRegistryPath = fileExtToUse; + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString appProgId; + if (NS_SUCCEEDED(regKey->ReadStringValue(u""_ns, appProgId))) { + // Bug 358297 - ignore the embedded internet explorer handler + if (appProgId != u"XPSViewer.Document"_ns) { + nsAutoString appFilesystemCommand; + if (GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand, + false) && + !IsPathInList(appFilesystemCommand, trackList)) { + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + regKey->Close(); + } + + // 2) list HKEY_CLASSES_ROOT\.ext\OpenWithList + + workingRegistryPath = fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithList"); + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 3) List HKEY_CLASSES_ROOT\.ext\OpenWithProgids, with the + // different step of resolving the progids for the command handler. + + workingRegistryPath = fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithProgids"); + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + // HKEY_CLASSES_ROOT\.ext\OpenWithProgids\Windows.XPSReachViewer + nsAutoString appProgId; + if (NS_FAILED(regKey->GetValueName(index, appProgId))) continue; + + nsAutoString appFilesystemCommand; + if (!GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 4) Add any non configured applications located in the MRU list + + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion + // \Explorer\FileExts\.ext\OpenWithList + workingRegistryPath = nsLiteralString( + u"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"); + workingRegistryPath += fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithList"); + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName, appValue; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + if (appName.EqualsLiteral("MRUList")) continue; + if (NS_FAILED(regKey->ReadStringValue(appName, appValue))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appValue, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + + // 5) Add any non configured progids in the MRU list, with the + // different step of resolving the progids for the command handler. + + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion + // \Explorer\FileExts\.ext\OpenWithProgids + workingRegistryPath = nsLiteralString( + u"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"); + workingRegistryPath += fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithProgids"); + + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appIndex, appProgId; + if (NS_FAILED(regKey->GetValueName(index, appProgId))) continue; + + nsAutoString appFilesystemCommand; + if (!GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 6) Check the perceived type value, and use this to lookup the + // perceivedtype open with list. + // http://msdn2.microsoft.com/en-us/library/aa969373.aspx + + workingRegistryPath = fileExtToUse; + + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString perceivedType; + rv = regKey->ReadStringValue(u"PerceivedType"_ns, perceivedType); + if (NS_SUCCEEDED(rv)) { + nsAutoString openWithListPath(u"SystemFileAssociations\\"_ns); + openWithListPath.Append(perceivedType); // no period + openWithListPath.AppendLiteral("\\OpenWithList"); + + nsresult rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + openWithListPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + } + } + } // extKnown == false + + // 7) list global HKEY_CLASSES_ROOT\*\OpenWithList + // Listing general purpose handlers, not specific to a mime type or file + // extension + + workingRegistryPath = u"*\\OpenWithList"_ns; + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 8) General application's list - not file extension specific on windows + workingRegistryPath = u"Applications"_ns; + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, workingRegistryPath, + nsIWindowsRegKey::ACCESS_ENUMERATE_SUB_KEYS | + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetChildCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetChildName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + + // Return to the caller + *_retval = appList; + NS_ADDREF(*_retval); + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::IsCurrentAppOSDefault(bool* _retval) { + *_retval = false; + nsCOMPtr defaultApp = GetDefaultApplication(); + if (defaultApp) { + // Determine if the default executable is our executable. + nsCOMPtr ourBinary; + XRE_GetBinaryPath(getter_AddRefs(ourBinary)); + bool isSame = false; + nsresult rv = defaultApp->Equals(ourBinary, &isSame); + if (NS_FAILED(rv)) { + return rv; + } + *_retval = isSame; + } + return NS_OK; +} diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.h b/uriloader/exthandler/win/nsMIMEInfoWin.h new file mode 100644 index 0000000000..fa972b23a9 --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.h @@ -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/. */ + +#ifndef nsMIMEInfoWin_h_ +#define nsMIMEInfoWin_h_ + +#include "nsMIMEInfoImpl.h" +#include "nsIPropertyBag.h" +#include "nsIMutableArray.h" +#include "nsTArray.h" + +class nsMIMEInfoWin : public nsMIMEInfoBase, public nsIPropertyBag { + virtual ~nsMIMEInfoWin(); + + public: + explicit nsMIMEInfoWin(const char* aType = "") : nsMIMEInfoBase(aType) {} + explicit nsMIMEInfoWin(const nsACString& aMIMEType) + : nsMIMEInfoBase(aMIMEType) {} + nsMIMEInfoWin(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoBase(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override; + NS_IMETHOD GetHasDefaultHandler(bool* _retval) override; + NS_IMETHOD GetPossibleLocalHandlers(nsIArray** _retval) override; + NS_IMETHOD IsCurrentAppOSDefault(bool* _retval) override; + + void UpdateDefaultInfoIfStale(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPROPERTYBAG + + void SetDefaultApplicationHandler(nsIFile* aDefaultApplication) { + mDefaultApplication = aDefaultApplication; + } + + protected: + nsIFile* GetDefaultApplication() { + UpdateDefaultInfoIfStale(); + return mDefaultApplication; + } + + virtual nsresult LoadUriInternal(nsIURI* aURI); + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile); + + private: + nsCOMPtr mDefaultApplication; + + // Given a path to a local handler, return its + // nsILocalHandlerApp instance. + bool GetLocalHandlerApp(const nsAString& aCommandHandler, + nsCOMPtr& aApp); + + // Return the cleaned up file path associated + // with a command verb located in root/Applications. + bool GetAppsVerbCommandHandler(const nsAString& appExeName, + nsAString& applicationPath, bool bEdit); + + // Return the cleaned up file path associated + // with a progid command verb located in root. + bool GetProgIDVerbCommandHandler(const nsAString& appProgIDName, + nsAString& applicationPath, bool bEdit); + + // Lookup a rundll command handler and return + // a populated command template for use with rundll32.exe. + bool GetDllLaunchInfo(nsIFile* aDll, nsIFile* aFile, nsAString& args, + bool bEdit); + + // Helper routine used in tracking app lists + void ProcessPath(nsCOMPtr& appList, + nsTArray& trackList, + const nsAString& appFilesystemCommand); + + // Helper routine to call mozilla::ShellExecuteByExplorer + nsresult ShellExecuteWithIFile(nsIFile* aExecutable, int aArgc, + const wchar_t** aArgv); +}; + +#endif diff --git a/uriloader/exthandler/win/nsOSHelperAppService.cpp b/uriloader/exthandler/win/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..8d23dd758d --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.cpp @@ -0,0 +1,612 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sts=2 sw=2 et cin: + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "nsComponentManagerUtils.h" +#include "nsOSHelperAppService.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsIMIMEInfo.h" +#include "nsMIMEInfoWin.h" +#include "nsMimeTypes.h" +#include "nsNativeCharsetUtils.h" +#include "nsLocalFile.h" +#include "nsIWindowsRegKey.h" +#include "nsXULAppAPI.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WindowsVersion.h" + +// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN +#include +#include + +#define LOG(...) MOZ_LOG(sLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) + +// helper methods: forward declarations... +static nsresult GetExtensionFromWindowsMimeDatabase(const nsACString& aMimeType, + nsString& aFileExtension); + +nsOSHelperAppService::nsOSHelperAppService() + : nsExternalHelperAppService(), mAppAssoc(nullptr) { + CoInitialize(nullptr); + CoCreateInstance(CLSID_ApplicationAssociationRegistration, nullptr, + CLSCTX_INPROC, IID_IApplicationAssociationRegistration, + (void**)&mAppAssoc); +} + +nsOSHelperAppService::~nsOSHelperAppService() { + if (mAppAssoc) mAppAssoc->Release(); + mAppAssoc = nullptr; + CoUninitialize(); +} + +// The windows registry provides a mime database key which lists a set of mime +// types and corresponding "Extension" values. we can use this to look up our +// mime type to see if there is a preferred extension for the mime type. +static nsresult GetExtensionFromWindowsMimeDatabase(const nsACString& aMimeType, + nsString& aFileExtension) { + nsAutoString mimeDatabaseKey; + mimeDatabaseKey.AssignLiteral("MIME\\Database\\Content Type\\"); + + AppendASCIItoUTF16(aMimeType, mimeDatabaseKey); + + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_NOT_AVAILABLE; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, mimeDatabaseKey, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + + if (NS_SUCCEEDED(rv)) + regKey->ReadStringValue(u"Extension"_ns, aFileExtension); + + return NS_OK; +} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + // look up the protocol scheme in the windows registry....if we find a match + // then we have a handler for it... + *aHandlerExists = false; + if (aProtocolScheme && *aProtocolScheme) { + NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE); + wchar_t* pResult = nullptr; + NS_ConvertASCIItoUTF16 scheme(aProtocolScheme); + // We are responsible for freeing returned strings. + HRESULT hr = mAppAssoc->QueryCurrentDefault(scheme.get(), AT_URLPROTOCOL, + AL_EFFECTIVE, &pResult); + if (SUCCEEDED(hr)) { + CoTaskMemFree(pResult); + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + nsDependentString(scheme.get()), + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) { + // Open will fail if the registry key path doesn't exist. + return NS_OK; + } + + bool hasValue; + rv = regKey->HasValue(u"URL Protocol"_ns, &hasValue); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + if (!hasValue) { + return NS_OK; + } + + *aHandlerExists = true; + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription( + const nsACString& aScheme, nsAString& _retval) { + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_NOT_AVAILABLE; + + NS_ConvertASCIItoUTF16 buf(aScheme); + + if (mozilla::IsWin8OrLater()) { + wchar_t result[1024]; + DWORD resultSize = 1024; + HRESULT hr = AssocQueryString(0x1000 /* ASSOCF_IS_PROTOCOL */, + ASSOCSTR_FRIENDLYAPPNAME, buf.get(), NULL, + result, &resultSize); + if (SUCCEEDED(hr)) { + _retval = result; + return NS_OK; + } + } + + NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE); + wchar_t* pResult = nullptr; + // We are responsible for freeing returned strings. + HRESULT hr = mAppAssoc->QueryCurrentDefault(buf.get(), AT_URLPROTOCOL, + AL_EFFECTIVE, &pResult); + if (SUCCEEDED(hr)) { + nsCOMPtr app; + nsAutoString appInfo(pResult); + CoTaskMemFree(pResult); + if (NS_SUCCEEDED(GetDefaultAppInfo(appInfo, _retval, getter_AddRefs(app)))) + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol( + const nsACString& aScheme, bool* _retval) { + *_retval = false; + + NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE); + + NS_ConvertASCIItoUTF16 buf(aScheme); + + // Find the progID + wchar_t* pResult = nullptr; + HRESULT hr = mAppAssoc->QueryCurrentDefault(buf.get(), AT_URLPROTOCOL, + AL_EFFECTIVE, &pResult); + if (FAILED(hr)) { + return NS_ERROR_FAILURE; + } + nsAutoString progID(pResult); + // We are responsible for freeing returned strings. + CoTaskMemFree(pResult); + + // Find the default executable. + nsAutoString description; + nsCOMPtr appFile; + nsresult rv = GetDefaultAppInfo(progID, description, getter_AddRefs(appFile)); + if (NS_FAILED(rv)) { + return rv; + } + // Determine if the default executable is our executable. + nsCOMPtr ourBinary; + XRE_GetBinaryPath(getter_AddRefs(ourBinary)); + bool isSame = false; + rv = appFile->Equals(ourBinary, &isSame); + if (NS_FAILED(rv)) { + return rv; + } + *_retval = isSame; + return NS_OK; +} + +// GetMIMEInfoFromRegistry: This function obtains the values of some of the +// nsIMIMEInfo attributes for the mimeType/extension associated with the input +// registry key. The default entry for that key is the name of a registry key +// under HKEY_CLASSES_ROOT. The default value for *that* key is the descriptive +// name of the type. The EditFlags value is a binary value; the low order bit +// of the third byte of which indicates that the user does not need to be +// prompted. +// +// This function sets only the Description attribute of the input nsIMIMEInfo. +/* static */ +nsresult nsOSHelperAppService::GetMIMEInfoFromRegistry(const nsString& fileType, + nsIMIMEInfo* pInfo) { + nsresult rv = NS_OK; + + NS_ENSURE_ARG(pInfo); + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_NOT_AVAILABLE; + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileType, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return NS_ERROR_FAILURE; + + // OK, the default value here is the description of the type. + nsAutoString description; + rv = regKey->ReadStringValue(u""_ns, description); + if (NS_SUCCEEDED(rv)) pInfo->SetDescription(description); + + return NS_OK; +} + +///////////////////////////////////////////////////////////////////////////////////////////////// +// method overrides used to gather information from the windows registry for +// various mime types. +//////////////////////////////////////////////////////////////////////////////////////////////// + +/// Looks up the type for the extension aExt and compares it to aType +/* static */ +bool nsOSHelperAppService::typeFromExtEquals(const char16_t* aExt, + const char* aType) { + if (!aType) return false; + nsAutoString fileExtToUse; + if (aExt[0] != char16_t('.')) fileExtToUse = char16_t('.'); + + fileExtToUse.Append(aExt); + + bool eq = false; + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return eq; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileExtToUse, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return eq; + + nsAutoString type; + rv = regKey->ReadStringValue(u"Content Type"_ns, type); + if (NS_SUCCEEDED(rv)) eq = type.LowerCaseEqualsASCII(aType); + + return eq; +} + +// The "real" name of a given helper app (as specified by the path to the +// executable file held in various registry keys) is stored n the VERSIONINFO +// block in the file's resources. We need to find the path to the executable +// and then retrieve the "FileDescription" field value from the file. +nsresult nsOSHelperAppService::GetDefaultAppInfo( + const nsAString& aAppInfo, nsAString& aDefaultDescription, + nsIFile** aDefaultApplication) { + nsAutoString handlerCommand; + + // If all else fails, use the file type key name, which will be + // something like "pngfile" for .pngs, "WMVFile" for .wmvs, etc. + aDefaultDescription = aAppInfo; + *aDefaultApplication = nullptr; + + if (aAppInfo.IsEmpty()) return NS_ERROR_FAILURE; + + // aAppInfo may be a file, file path, program id, or + // Applications reference - + // c:\dir\app.exe + // Applications\appfile.exe/dll (shell\open...) + // ProgID.progid (shell\open...) + + nsAutoString handlerKeyName(aAppInfo); + + nsCOMPtr chkKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!chkKey) return NS_ERROR_FAILURE; + + nsresult rv = + chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, handlerKeyName, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) { + // It's a file system path to a handler + handlerCommand.Assign(aAppInfo); + } else { + handlerKeyName.AppendLiteral("\\shell\\open\\command"); + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_FAILURE; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, handlerKeyName, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return NS_ERROR_FAILURE; + + // OK, the default value here is the description of the type. + rv = regKey->ReadStringValue(u""_ns, handlerCommand); + if (NS_FAILED(rv)) { + // Check if there is a DelegateExecute string + nsAutoString delegateExecute; + rv = regKey->ReadStringValue(u"DelegateExecute"_ns, delegateExecute); + NS_ENSURE_SUCCESS(rv, rv); + + // Look for InProcServer32 + nsAutoString delegateExecuteRegPath; + delegateExecuteRegPath.AssignLiteral("CLSID\\"); + delegateExecuteRegPath.Append(delegateExecute); + delegateExecuteRegPath.AppendLiteral("\\InProcServer32"); + rv = chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + delegateExecuteRegPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + rv = chkKey->ReadStringValue(u""_ns, handlerCommand); + } + + if (NS_FAILED(rv)) { + // Look for LocalServer32 + delegateExecuteRegPath.AssignLiteral("CLSID\\"); + delegateExecuteRegPath.Append(delegateExecute); + delegateExecuteRegPath.AppendLiteral("\\LocalServer32"); + rv = chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + delegateExecuteRegPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + NS_ENSURE_SUCCESS(rv, rv); + rv = chkKey->ReadStringValue(u""_ns, handlerCommand); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + // XXX FIXME: If this fails, the UI will display the full command + // string. + // There are some rare cases this can happen - ["url.dll" -foo] + // for example won't resolve correctly to the system dir. The + // subsequent launch of the helper app will work though. + nsCOMPtr lf = new nsLocalFile(); + rv = lf->InitWithCommandLine(handlerCommand); + NS_ENSURE_SUCCESS(rv, rv); + lf.forget(aDefaultApplication); + + wchar_t friendlyName[1024]; + DWORD friendlyNameSize = 1024; + HRESULT hr = AssocQueryString(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME, + PromiseFlatString(aAppInfo).get(), NULL, + friendlyName, &friendlyNameSize); + if (SUCCEEDED(hr) && friendlyNameSize > 1) { + aDefaultDescription.Assign(friendlyName, friendlyNameSize - 1); + } + + return NS_OK; +} + +already_AddRefed nsOSHelperAppService::GetByExtension( + const nsString& aFileExt, const char* aTypeHint) { + if (aFileExt.IsEmpty()) return nullptr; + + // Determine the mime type. + nsAutoCString typeToUse; + if (aTypeHint && *aTypeHint) { + typeToUse.Assign(aTypeHint); + } else if (!GetMIMETypeFromOSForExtension(NS_ConvertUTF16toUTF8(aFileExt), + typeToUse)) { + return nullptr; + } + + RefPtr mimeInfo = new nsMIMEInfoWin(typeToUse); + + // Our extension APIs expect extensions without the '.', so normalize: + uint32_t dotlessIndex = aFileExt.First() != char16_t('.') ? 0 : 1; + nsAutoCString lowerFileExt = + NS_ConvertUTF16toUTF8(Substring(aFileExt, dotlessIndex)); + ToLowerCase(lowerFileExt); + mimeInfo->AppendExtension(lowerFileExt); + mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + + if (NS_FAILED(InternalSetDefaultsOnMIME(mimeInfo))) { + return nullptr; + } + + return mimeInfo.forget(); +} + +nsresult nsOSHelperAppService::InternalSetDefaultsOnMIME( + nsMIMEInfoWin* aMIMEInfo) { + NS_ENSURE_ARG(aMIMEInfo); + + nsAutoCString primaryExt; + aMIMEInfo->GetPrimaryExtension(primaryExt); + + if (primaryExt.IsEmpty()) { + return NS_ERROR_FAILURE; + } + + // windows registry assumes your file extension is going to include the '.', + // but our APIs don't have it, so add it: + nsAutoString assocType = NS_ConvertUTF8toUTF16(primaryExt); + if (assocType.First() != char16_t('.')) { + assocType.Insert(char16_t('.'), 0); + } + + nsAutoString appInfo; + bool found; + + // Retrieve the default application for this extension + NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE); + wchar_t* pResult = nullptr; + HRESULT hr = mAppAssoc->QueryCurrentDefault(assocType.get(), AT_FILEEXTENSION, + AL_EFFECTIVE, &pResult); + if (SUCCEEDED(hr)) { + found = true; + appInfo.Assign(pResult); + CoTaskMemFree(pResult); + } else { + found = false; + } + + // Bug 358297 - ignore the default handler, force the user to choose app + if (appInfo.EqualsLiteral("XPSViewer.Document")) found = false; + + if (!found) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Get other nsIMIMEInfo fields from registry, if possible. + nsAutoString defaultDescription; + nsCOMPtr defaultApplication; + + if (NS_FAILED(GetDefaultAppInfo(appInfo, defaultDescription, + getter_AddRefs(defaultApplication)))) { + return NS_ERROR_NOT_AVAILABLE; + } + + aMIMEInfo->SetDefaultDescription(defaultDescription); + aMIMEInfo->SetDefaultApplicationHandler(defaultApplication); + + // Grab the general description + GetMIMEInfoFromRegistry(appInfo, aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound, nsIMIMEInfo** aMIMEInfo) { + *aFound = false; + + const nsCString& flatType = PromiseFlatCString(aMIMEType); + nsAutoString fileExtension; + CopyUTF8toUTF16(aFileExt, fileExtension); + + /* XXX The octet-stream check is a gross hack to wallpaper over the most + * common Win32 extension issues caused by the fix for bug 116938. See bug + * 120327, comment 271 for why this is needed. Not even sure we + * want to remove this once we have fixed all this stuff to work + * right; any info we get from the OS on this type is pretty much + * useless.... + */ + bool haveMeaningfulMimeType = + !aMIMEType.IsEmpty() && + !aMIMEType.LowerCaseEqualsLiteral(APPLICATION_OCTET_STREAM); + LOG("Extension lookup on '%S' with mimetype '%s'%s\n", + static_cast(fileExtension.get()), flatType.get(), + haveMeaningfulMimeType ? " (treated as meaningful)" : ""); + + RefPtr mi; + + // We should have *something* to go on here. + nsAutoString extensionFromMimeType; + if (haveMeaningfulMimeType) { + GetExtensionFromWindowsMimeDatabase(aMIMEType, extensionFromMimeType); + } + if (fileExtension.IsEmpty() && extensionFromMimeType.IsEmpty()) { + // Without an extension from the mimetype or the file, we can't + // do anything here. + mi = new nsMIMEInfoWin(flatType.get()); + mi.forget(aMIMEInfo); + return NS_OK; + } + + // Either fileExtension or extensionFromMimeType must now be non-empty. + + *aFound = true; + + // On Windows, we prefer the file extension for lookups over the mimetype, + // because that's how windows does things. + // If we have no file extension or it doesn't match the mimetype, use the + // mime type's default file extension instead. + bool usedMimeTypeExtensionForLookup = false; + if (fileExtension.IsEmpty() || + (!extensionFromMimeType.IsEmpty() && + !typeFromExtEquals(fileExtension.get(), flatType.get()))) { + usedMimeTypeExtensionForLookup = true; + fileExtension = extensionFromMimeType; + LOG("Now using '%s' mimetype's default file extension '%S' for lookup\n", + flatType.get(), static_cast(fileExtension.get())); + } + + // If we have an extension, use it for lookup: + mi = GetByExtension(fileExtension, flatType.get()); + LOG("Extension lookup on '%S' found: 0x%p\n", + static_cast(fileExtension.get()), mi.get()); + + if (mi) { + bool hasDefault = false; + mi->GetHasDefaultHandler(&hasDefault); + // If we don't find a default handler description, see if we can find one + // using the mimetype. + if (!hasDefault && !usedMimeTypeExtensionForLookup) { + RefPtr miFromMimeType = + GetByExtension(extensionFromMimeType, flatType.get()); + LOG("Mime-based ext. lookup for '%S' found 0x%p\n", + static_cast(extensionFromMimeType.get()), + miFromMimeType.get()); + if (miFromMimeType) { + nsAutoString desc; + miFromMimeType->GetDefaultDescription(desc); + mi->SetDefaultDescription(desc); + } + } + mi.forget(aMIMEInfo); + return NS_OK; + } + + // The extension didn't work. Try the extension from the mimetype if + // different: + if (!extensionFromMimeType.IsEmpty() && !usedMimeTypeExtensionForLookup) { + mi = GetByExtension(extensionFromMimeType, flatType.get()); + LOG("Mime-based ext. lookup for '%S' found 0x%p\n", + static_cast(extensionFromMimeType.get()), mi.get()); + } + if (mi) { + mi.forget(aMIMEInfo); + return NS_OK; + } + // This didn't work either, so just return an empty dummy mimeinfo. + *aFound = false; + mi = new nsMIMEInfoWin(flatType.get()); + // If we didn't resort to the mime type's extension, we must have had a + // valid extension, so stick its lowercase version on the mime info. + if (!usedMimeTypeExtensionForLookup) { + nsAutoCString lowerFileExt; + ToLowerCase(aFileExt, lowerFileExt); + mi->AppendExtension(lowerFileExt); + } + mi.forget(aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::UpdateDefaultAppInfo(nsIMIMEInfo* aMIMEInfo) { + InternalSetDefaultsOnMIME(static_cast(aMIMEInfo)); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) { + NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); + + nsresult rv = + OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found); + if (NS_FAILED(rv)) return rv; + + nsMIMEInfoWin* handlerInfo = + new nsMIMEInfoWin(aScheme, nsMIMEInfoBase::eProtocolInfo); + NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(*_retval = handlerInfo); + + if (!*found) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + return NS_OK; + } + + nsAutoString desc; + GetApplicationDescription(aScheme, desc); + handlerInfo->SetDefaultDescription(desc); + + return NS_OK; +} + +bool nsOSHelperAppService::GetMIMETypeFromOSForExtension( + const nsACString& aExtension, nsACString& aMIMEType) { + if (aExtension.IsEmpty()) return false; + + // windows registry assumes your file extension is going to include the '.'. + // so make sure it's there... + nsAutoString fileExtToUse; + if (aExtension.First() != '.') fileExtToUse = char16_t('.'); + + AppendUTF8toUTF16(aExtension, fileExtToUse); + + // Try to get an entry from the windows registry. + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return false; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileExtToUse, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString mimeType; + if (NS_FAILED(regKey->ReadStringValue(u"Content Type"_ns, mimeType)) || + mimeType.IsEmpty()) { + return false; + } + // Content-Type is always in ASCII + aMIMEType.Truncate(); + LossyAppendUTF16toASCII(mimeType, aMIMEType); + return true; +} diff --git a/uriloader/exthandler/win/nsOSHelperAppService.h b/uriloader/exthandler/win/nsOSHelperAppService.h new file mode 100644 index 0000000000..e2bce86d5b --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 3; 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 nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is +// implemented on each platform. It contains platform specific code for finding +// helper applications for a given mime type in addition to launching those +// applications. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" +#include + +#ifdef _WIN32_WINNT +# undef _WIN32_WINNT +#endif +#define _WIN32_WINNT 0x0600 +#include + +class nsMIMEInfoWin; +class nsIMIMEInfo; + +class nsOSHelperAppService : public nsExternalHelperAppService { + public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) override; + nsresult LoadUriInternal(nsIURI* aURL); + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + // method overrides for windows registry look up steps.... + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + NS_IMETHOD UpdateDefaultAppInfo(nsIMIMEInfo* aMIMEInfo) override; + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval); + virtual bool GetMIMETypeFromOSForExtension(const nsACString& aExtension, + nsACString& aMIMEType) override; + + /** Get the string value of a registry value and store it in result. + * @return true on success, false on failure + */ + static bool GetValueString(HKEY hKey, const char16_t* pValueName, + nsAString& result); + + protected: + nsresult GetDefaultAppInfo(const nsAString& aTypeName, + nsAString& aDefaultDescription, + nsIFile** aDefaultApplication); + // Lookup a mime info by extension, using an optional type hint + already_AddRefed GetByExtension( + const nsString& aFileExt, const char* aTypeHint = nullptr); + nsresult InternalSetDefaultsOnMIME(nsMIMEInfoWin* aMIMEInfo); + nsresult FindOSMimeInfoForType(const char* aMimeContentType, nsIURI* aURI, + char** aFileExtension, + nsIMIMEInfo** aMIMEInfo); + + static nsresult GetMIMEInfoFromRegistry(const nsString& fileType, + nsIMIMEInfo* pInfo); + /// Looks up the type for the extension aExt and compares it to aType + static bool typeFromExtEquals(const char16_t* aExt, const char* aType); + + private: + IApplicationAssociationRegistration* mAppAssoc; +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/moz.build b/uriloader/moz.build new file mode 100644 index 0000000000..54cda599f8 --- /dev/null +++ b/uriloader/moz.build @@ -0,0 +1,20 @@ +# -*- 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["/uriloader"] = "docs" + +with Files("moz.build"): + BUG_COMPONENT = ("Firefox Build System", "General") + +with Files("docs/**"): + BUG_COMPONENT = ("Firefox", "File Handling") + +DIRS += [ + "base", + "exthandler", + "prefetch", + "preload", +] diff --git a/uriloader/prefetch/moz.build b/uriloader/prefetch/moz.build new file mode 100644 index 0000000000..8185124da0 --- /dev/null +++ b/uriloader/prefetch/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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking: Cache") + +XPIDL_SOURCES += [ + "nsIPrefetchService.idl", +] + +XPIDL_MODULE = "prefetch" + +UNIFIED_SOURCES += [ + "nsPrefetchService.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/base", +] diff --git a/uriloader/prefetch/nsIPrefetchService.idl b/uriloader/prefetch/nsIPrefetchService.idl new file mode 100644 index 0000000000..160d051116 --- /dev/null +++ b/uriloader/prefetch/nsIPrefetchService.idl @@ -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/. */ + +#include "nsISupports.idl" +#include "nsIContentPolicy.idl" + +interface nsIURI; +interface nsISimpleEnumerator; +interface nsIReferrerInfo; + +webidl Node; + +[scriptable, uuid(422a1807-4e7f-463d-b8d7-ca2ceb9b5d53)] +interface nsIPrefetchService : nsISupports +{ + /** + * Enqueue a request to prefetch the specified URI. + * + * @param aURI the URI of the document to prefetch + * @param aReferrerInfo the referrerInfo of the request + * @param aSource the DOM node (such as a tag) that requested this + * fetch, or null if the prefetch was not requested by a DOM node. + * @param aExplicit the link element has an explicit prefetch link type + */ + void prefetchURI(in nsIURI aURI, + in nsIReferrerInfo aReferrerInfo, + in Node aSource, + in boolean aExplicit); + + /** + * Start a preload of the specified URI. + * + * @param aURI the URI of the document to preload + * @param aReferrerInfo the referrerInfo of the request + * @param aSource the DOM node (such as a tag) that requested this + * fetch, or null if the prefetch was not requested by a DOM node. + * @param aPolicyType content policy to be used for this load. + */ + void preloadURI(in nsIURI aURI, + in nsIReferrerInfo aReferrerInfo, + in Node aSource, + in nsContentPolicyType aPolicyType); + + /** + * Find out if there are any prefetches running or queued + */ + boolean hasMoreElements(); + + /** + * Cancel prefetch or preload for a Node. + */ + void cancelPrefetchPreloadURI(in nsIURI aURI, in Node aSource); +}; diff --git a/uriloader/prefetch/nsPrefetchService.cpp b/uriloader/prefetch/nsPrefetchService.cpp new file mode 100644 index 0000000000..635e4e34a9 --- /dev/null +++ b/uriloader/prefetch/nsPrefetchService.cpp @@ -0,0 +1,895 @@ +/* -*- 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 "nsPrefetchService.h" + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "mozilla/CORSMode.h" +#include "mozilla/Components.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/HTMLLinkElement.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/Preferences.h" +#include "ReferrerInfo.h" + +#include "nsIObserverService.h" +#include "nsIWebProgress.h" +#include "nsICacheInfoChannel.h" +#include "nsIHttpChannel.h" +#include "nsIURL.h" +#include "nsISupportsPriority.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsStreamUtils.h" +#include "prtime.h" +#include "mozilla/Logging.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsINode.h" +#include "mozilla/dom/Document.h" +#include "nsContentUtils.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "nsICachingChannel.h" +#include "nsHttp.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsPrefetch:5 +// set MOZ_LOG_FILE=prefetch.log +// +// this enables LogLevel::Debug level information and places all output in +// the file prefetch.log +// +static LazyLogModule gPrefetchLog("nsPrefetch"); + +#undef LOG +#define LOG(args) MOZ_LOG(gPrefetchLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gPrefetchLog, mozilla::LogLevel::Debug) + +#define PREFETCH_PREF "network.prefetch-next" +#define PARALLELISM_PREF "network.prefetch-next.parallelism" +#define AGGRESSIVE_PREF "network.prefetch-next.aggressive" + +//----------------------------------------------------------------------------- +// nsPrefetchNode +//----------------------------------------------------------------------------- + +nsPrefetchNode::nsPrefetchNode(nsPrefetchService* aService, nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, nsINode* aSource, + nsContentPolicyType aPolicyType, bool aPreload) + : mURI(aURI), + mReferrerInfo(aReferrerInfo), + mPolicyType(aPolicyType), + mPreload(aPreload), + mService(aService), + mChannel(nullptr), + mBytesRead(0), + mShouldFireLoadEvent(false) { + nsWeakPtr source = do_GetWeakReference(aSource); + mSources.AppendElement(source); +} + +nsresult nsPrefetchNode::OpenChannel() { + if (mSources.IsEmpty()) { + // Don't attempt to prefetch if we don't have a source node + // (which should never happen). + return NS_ERROR_FAILURE; + } + nsCOMPtr source; + while (!mSources.IsEmpty() && + !(source = do_QueryReferent(mSources.ElementAt(0)))) { + // If source is null remove it. + // (which should never happen). + mSources.RemoveElementAt(0); + } + + if (!source) { + // Don't attempt to prefetch if we don't have a source node + // (which should never happen). + + return NS_ERROR_FAILURE; + } + nsCOMPtr loadGroup = source->OwnerDoc()->GetDocumentLoadGroup(); + CORSMode corsMode = CORS_NONE; + if (auto* link = dom::HTMLLinkElement::FromNode(source)) { + corsMode = link->GetCORSMode(); + } + + uint32_t securityFlags; + if (corsMode == CORS_NONE) { + securityFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; + } else { + securityFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; + if (corsMode == CORS_USE_CREDENTIALS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } + } + nsresult rv = NS_NewChannelInternal( + getter_AddRefs(mChannel), mURI, source, source->NodePrincipal(), + nullptr, // aTriggeringPrincipal + Maybe(), Maybe(), securityFlags, + mPolicyType, source->OwnerDoc()->CookieJarSettings(), + nullptr, // aPerformanceStorage + loadGroup, // aLoadGroup + this, // aCallbacks + nsIRequest::LOAD_BACKGROUND | nsICachingChannel::LOAD_ONLY_IF_MODIFIED); + + NS_ENSURE_SUCCESS(rv, rv); + + // configure HTTP specific stuff + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + DebugOnly success = httpChannel->SetReferrerInfo(mReferrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(success)); + + // https://fetch.spec.whatwg.org/#http-sec-purpose + success = + httpChannel->SetRequestHeader("Sec-Purpose"_ns, "prefetch"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(success)); + } + + // Reduce the priority of prefetch network requests. + nsCOMPtr priorityChannel = do_QueryInterface(mChannel); + if (priorityChannel) { + priorityChannel->AdjustPriority(nsISupportsPriority::PRIORITY_LOWEST); + } + + rv = mChannel->AsyncOpen(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Drop the ref to the channel, because we don't want to end up with + // cycles through it. + mChannel = nullptr; + } + return rv; +} + +nsresult nsPrefetchNode::CancelChannel(nsresult error) { + mChannel->Cancel(error); + mChannel = nullptr; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsPrefetchNode, nsIRequestObserver, nsIStreamListener, + nsIInterfaceRequestor, nsIChannelEventSink, + nsIRedirectResultListener) + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::OnStartRequest(nsIRequest* aRequest) { + nsresult rv; + + nsCOMPtr httpChannel = do_QueryInterface(aRequest, &rv); + if (NS_FAILED(rv)) return rv; + + // if the load is cross origin without CORS, or the CORS access is rejected, + // always fire load event to avoid leaking site information. + nsCOMPtr loadInfo = httpChannel->LoadInfo(); + mShouldFireLoadEvent = + loadInfo->GetTainting() == LoadTainting::Opaque || + (loadInfo->GetTainting() == LoadTainting::CORS && + (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv))); + + // no need to prefetch http error page + bool requestSucceeded; + if (NS_FAILED(httpChannel->GetRequestSucceeded(&requestSucceeded)) || + !requestSucceeded) { + return NS_BINDING_ABORTED; + } + + nsCOMPtr cacheInfoChannel = + do_QueryInterface(aRequest, &rv); + if (NS_FAILED(rv)) return rv; + + // no need to prefetch a document that is already in the cache + bool fromCache; + if (NS_SUCCEEDED(cacheInfoChannel->IsFromCache(&fromCache)) && fromCache) { + LOG(("document is already in the cache; canceling prefetch\n")); + // although it's canceled we still want to fire load event + mShouldFireLoadEvent = true; + return NS_BINDING_ABORTED; + } + + // + // no need to prefetch a document that must be requested fresh each + // and every time. + // + uint32_t expTime; + if (NS_SUCCEEDED(cacheInfoChannel->GetCacheTokenExpirationTime(&expTime))) { + if (mozilla::net::NowInSeconds() >= expTime) { + LOG( + ("document cannot be reused from cache; " + "canceling prefetch\n")); + return NS_BINDING_ABORTED; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchNode::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + uint32_t bytesRead = 0; + aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead); + mBytesRead += bytesRead; + LOG(("prefetched %u bytes [offset=%" PRIu64 "]\n", bytesRead, aOffset)); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchNode::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + LOG(("done prefetching [status=%" PRIx32 "]\n", + static_cast(aStatus))); + + if (mBytesRead == 0 && aStatus == NS_OK && mChannel) { + // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was + // specified), but the object should report loadedSize as if it + // did. + mChannel->GetContentLength(&mBytesRead); + } + + mService->NotifyLoadCompleted(this); + mService->DispatchEvent(this, mShouldFireLoadEvent || NS_SUCCEEDED(aStatus)); + mService->RemoveNodeAndMaybeStartNextPrefetchURI(this); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + NS_ADDREF_THIS(); + *aResult = static_cast(this); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { + NS_ADDREF_THIS(); + *aResult = static_cast(this); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* callback) { + nsCOMPtr newURI; + nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) return rv; + + if (!newURI->SchemeIs("http") && !newURI->SchemeIs("https")) { + LOG(("rejected: URL is not of type http/https\n")); + return NS_ERROR_ABORT; + } + + // HTTP request headers are not automatically forwarded to the new channel. + nsCOMPtr httpChannel = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(httpChannel); + + // https://fetch.spec.whatwg.org/#http-sec-purpose + rv = httpChannel->SetRequestHeader("Sec-Purpose"_ns, "prefetch"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Assign to mChannel after we get notification about success of the + // redirect in OnRedirectResult. + mRedirectChannel = aNewChannel; + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIRedirectResultListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::OnRedirectResult(nsresult status) { + if (NS_SUCCEEDED(status) && mRedirectChannel) mChannel = mRedirectChannel; + + mRedirectChannel = nullptr; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService +//----------------------------------------------------------------------------- + +nsPrefetchService::nsPrefetchService() + : mMaxParallelism(6), + mStopCount(0), + mHaveProcessed(false), + mPrefetchDisabled(true), + mAggressive(false) {} + +nsPrefetchService::~nsPrefetchService() { + Preferences::RemoveObserver(this, PREFETCH_PREF); + Preferences::RemoveObserver(this, PARALLELISM_PREF); + Preferences::RemoveObserver(this, AGGRESSIVE_PREF); + // cannot reach destructor if prefetch in progress (listener owns reference + // to this service) + EmptyPrefetchQueue(); +} + +nsresult nsPrefetchService::Init() { + nsresult rv; + + // read prefs and hook up pref observer + mPrefetchDisabled = !Preferences::GetBool(PREFETCH_PREF, !mPrefetchDisabled); + Preferences::AddWeakObserver(this, PREFETCH_PREF); + + mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism); + if (mMaxParallelism < 1) { + mMaxParallelism = 1; + } + Preferences::AddWeakObserver(this, PARALLELISM_PREF); + + mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false); + Preferences::AddWeakObserver(this, AGGRESSIVE_PREF); + + // Observe xpcom-shutdown event + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (!observerService) return NS_ERROR_FAILURE; + + rv = observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mPrefetchDisabled) { + AddProgressListener(); + } + + return NS_OK; +} + +void nsPrefetchService::RemoveNodeAndMaybeStartNextPrefetchURI( + nsPrefetchNode* aFinished) { + if (aFinished) { + mCurrentNodes.RemoveElement(aFinished); + } + + if ((!mStopCount && mHaveProcessed) || mAggressive) { + ProcessNextPrefetchURI(); + } +} + +void nsPrefetchService::ProcessNextPrefetchURI() { + if (mCurrentNodes.Length() >= static_cast(mMaxParallelism)) { + // We already have enough prefetches going on, so hold off + // for now. + return; + } + + nsresult rv; + + do { + if (mPrefetchQueue.empty()) { + break; + } + RefPtr node = std::move(mPrefetchQueue.front()); + mPrefetchQueue.pop_front(); + + if (LOG_ENABLED()) { + LOG(("ProcessNextPrefetchURI [%s]\n", + node->mURI->GetSpecOrDefault().get())); + } + + // + // if opening the channel fails (e.g. security check returns an error), + // send an error event and then just skip to the next uri + // + rv = node->OpenChannel(); + if (NS_SUCCEEDED(rv)) { + mCurrentNodes.AppendElement(node); + } else { + DispatchEvent(node, false); + } + } while (NS_FAILED(rv)); +} + +void nsPrefetchService::NotifyLoadRequested(nsPrefetchNode* node) { + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (!observerService) return; + + observerService->NotifyObservers( + static_cast(node), + (node->mPreload) ? "preload-load-requested" : "prefetch-load-requested", + nullptr); +} + +void nsPrefetchService::NotifyLoadCompleted(nsPrefetchNode* node) { + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (!observerService) return; + + observerService->NotifyObservers( + static_cast(node), + (node->mPreload) ? "preload-load-completed" : "prefetch-load-completed", + nullptr); +} + +void nsPrefetchService::DispatchEvent(nsPrefetchNode* node, bool aSuccess) { + for (uint32_t i = 0; i < node->mSources.Length(); i++) { + nsCOMPtr domNode = do_QueryReferent(node->mSources.ElementAt(i)); + if (domNode && domNode->IsInComposedDoc()) { + // We don't dispatch synchronously since |node| might be in a DocGroup + // that we're not allowed to touch. (Our network request happens in the + // DocGroup of one of the mSources nodes--not necessarily this one). + RefPtr dispatcher = new AsyncEventDispatcher( + domNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo); + dispatcher->RequireNodeInDocument(); + dispatcher->PostDOMEvent(); + } + } +} + +//----------------------------------------------------------------------------- +// nsPrefetchService +//----------------------------------------------------------------------------- + +void nsPrefetchService::AddProgressListener() { + // Register as an observer for the document loader + nsCOMPtr progress = components::DocLoader::Service(); + if (progress) + progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT); +} + +void nsPrefetchService::RemoveProgressListener() { + // Register as an observer for the document loader + nsCOMPtr progress = components::DocLoader::Service(); + if (progress) progress->RemoveProgressListener(this); +} + +nsresult nsPrefetchService::EnqueueURI(nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, + nsPrefetchNode** aNode) { + RefPtr node = new nsPrefetchNode( + this, aURI, aReferrerInfo, aSource, nsIContentPolicy::TYPE_OTHER, false); + mPrefetchQueue.push_back(node); + node.forget(aNode); + return NS_OK; +} + +void nsPrefetchService::EmptyPrefetchQueue() { + while (!mPrefetchQueue.empty()) { + mPrefetchQueue.pop_back(); + } +} + +void nsPrefetchService::StartPrefetching() { + // + // at initialization time we might miss the first DOCUMENT START + // notification, so we have to be careful to avoid letting our + // stop count go negative. + // + if (mStopCount > 0) mStopCount--; + + LOG(("StartPrefetching [stopcount=%d]\n", mStopCount)); + + // only start prefetching after we've received enough DOCUMENT + // STOP notifications. we do this inorder to defer prefetching + // until after all sub-frames have finished loading. + if (!mStopCount) { + mHaveProcessed = true; + while (!mPrefetchQueue.empty() && + mCurrentNodes.Length() < static_cast(mMaxParallelism)) { + ProcessNextPrefetchURI(); + } + } +} + +void nsPrefetchService::StopPrefetching() { + mStopCount++; + + LOG(("StopPrefetching [stopcount=%d]\n", mStopCount)); + + // When we start a load, we need to stop all prefetches that has been + // added by the old load, therefore call StopAll only at the moment we + // switch to a new page load (i.e. mStopCount == 1). + // TODO: do not stop prefetches that are relevant for the new load. + if (mStopCount == 1) { + StopAll(); + } +} + +void nsPrefetchService::StopCurrentPrefetchsPreloads(bool aPreload) { + for (int32_t i = mCurrentNodes.Length() - 1; i >= 0; --i) { + if (mCurrentNodes[i]->mPreload == aPreload) { + mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED); + mCurrentNodes.RemoveElementAt(i); + } + } + + if (!aPreload) { + EmptyPrefetchQueue(); + } +} + +void nsPrefetchService::StopAll() { + for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { + mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED); + } + mCurrentNodes.Clear(); + EmptyPrefetchQueue(); +} + +nsresult nsPrefetchService::CheckURIScheme(nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo) { + // + // XXX we should really be asking the protocol handler if it supports + // caching, so we can determine if there is any value to prefetching. + // for now, we'll only prefetch http and https links since we know that's + // the most common case. + // + if (!aURI->SchemeIs("http") && !aURI->SchemeIs("https")) { + LOG(("rejected: URL is not of type http/https\n")); + return NS_ERROR_ABORT; + } + + // + // the referrer URI must be http: + // + nsCOMPtr referrer = aReferrerInfo->GetOriginalReferrer(); + if (!referrer) { + return NS_ERROR_ABORT; + } + + if (!referrer->SchemeIs("http") && !referrer->SchemeIs("https")) { + LOG(("rejected: referrer URL is neither http nor https\n")); + return NS_ERROR_ABORT; + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsPrefetchService, nsIPrefetchService, nsIWebProgressListener, + nsIObserver, nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsIPrefetchService +//----------------------------------------------------------------------------- + +nsresult nsPrefetchService::Preload(nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, + nsContentPolicyType aPolicyType) { + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aReferrerInfo); + if (LOG_ENABLED()) { + LOG(("PreloadURI [%s]\n", aURI->GetSpecOrDefault().get())); + } + + LOG(("rejected: preload service is deprecated\n")); + return NS_ERROR_ABORT; +} + +nsresult nsPrefetchService::Prefetch(nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, bool aExplicit) { + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aReferrerInfo); + + if (LOG_ENABLED()) { + LOG(("PrefetchURI [%s]\n", aURI->GetSpecOrDefault().get())); + } + + if (mPrefetchDisabled) { + LOG(("rejected: prefetch service is disabled\n")); + return NS_ERROR_ABORT; + } + + nsresult rv = CheckURIScheme(aURI, aReferrerInfo); + if (NS_FAILED(rv)) { + return rv; + } + + // XXX we might want to either leverage nsIProtocolHandler::protocolFlags + // or possibly nsIRequest::loadFlags to determine if this URI should be + // prefetched. + // + + // skip URLs that contain query strings, except URLs for which prefetching + // has been explicitly requested. + if (!aExplicit) { + nsCOMPtr url(do_QueryInterface(aURI, &rv)); + if (NS_FAILED(rv)) return rv; + nsAutoCString query; + rv = url->GetQuery(query); + if (NS_FAILED(rv) || !query.IsEmpty()) { + LOG(("rejected: URL has a query string\n")); + return NS_ERROR_ABORT; + } + } + + // + // Check whether it is being prefetched. + // + for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { + bool equals; + if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && equals) { + nsWeakPtr source = do_GetWeakReference(aSource); + if (mCurrentNodes[i]->mSources.IndexOf(source) == + mCurrentNodes[i]->mSources.NoIndex) { + LOG( + ("URL is already being prefetched, add a new reference " + "document\n")); + mCurrentNodes[i]->mSources.AppendElement(source); + return NS_OK; + } else { + LOG(("URL is already being prefetched by this document")); + return NS_ERROR_ABORT; + } + } + } + + // + // Check whether it is on the prefetch queue. + // + for (std::deque>::iterator nodeIt = + mPrefetchQueue.begin(); + nodeIt != mPrefetchQueue.end(); nodeIt++) { + bool equals; + RefPtr node = nodeIt->get(); + if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) { + nsWeakPtr source = do_GetWeakReference(aSource); + if (node->mSources.IndexOf(source) == node->mSources.NoIndex) { + LOG( + ("URL is already being prefetched, add a new reference " + "document\n")); + node->mSources.AppendElement(do_GetWeakReference(aSource)); + return NS_OK; + } else { + LOG(("URL is already being prefetched by this document")); + return NS_ERROR_ABORT; + } + } + } + + RefPtr enqueuedNode; + rv = EnqueueURI(aURI, aReferrerInfo, aSource, getter_AddRefs(enqueuedNode)); + NS_ENSURE_SUCCESS(rv, rv); + + NotifyLoadRequested(enqueuedNode); + + // if there are no pages loading, kick off the request immediately + if ((!mStopCount && mHaveProcessed) || mAggressive) { + ProcessNextPrefetchURI(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::CancelPrefetchPreloadURI(nsIURI* aURI, nsINode* aSource) { + NS_ENSURE_ARG_POINTER(aURI); + + if (LOG_ENABLED()) { + LOG(("CancelPrefetchURI [%s]\n", aURI->GetSpecOrDefault().get())); + } + + // + // look in current prefetches + // + for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { + bool equals; + if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && equals) { + nsWeakPtr source = do_GetWeakReference(aSource); + if (mCurrentNodes[i]->mSources.IndexOf(source) != + mCurrentNodes[i]->mSources.NoIndex) { + mCurrentNodes[i]->mSources.RemoveElement(source); + if (mCurrentNodes[i]->mSources.IsEmpty()) { + mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED); + mCurrentNodes.RemoveElementAt(i); + } + return NS_OK; + } + return NS_ERROR_FAILURE; + } + } + + // + // look into the prefetch queue + // + for (std::deque>::iterator nodeIt = + mPrefetchQueue.begin(); + nodeIt != mPrefetchQueue.end(); nodeIt++) { + bool equals; + RefPtr node = nodeIt->get(); + if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) { + nsWeakPtr source = do_GetWeakReference(aSource); + if (node->mSources.IndexOf(source) != node->mSources.NoIndex) { +#ifdef DEBUG + int32_t inx = node->mSources.IndexOf(source); + nsCOMPtr domNode = + do_QueryReferent(node->mSources.ElementAt(inx)); + MOZ_ASSERT(domNode); +#endif + + node->mSources.RemoveElement(source); + if (node->mSources.IsEmpty()) { + mPrefetchQueue.erase(nodeIt); + } + return NS_OK; + } + return NS_ERROR_FAILURE; + } + } + + // not found! + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsPrefetchService::PreloadURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, + nsContentPolicyType aPolicyType) { + return Preload(aURI, aReferrerInfo, aSource, aPolicyType); +} + +NS_IMETHODIMP +nsPrefetchService::PrefetchURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, bool aExplicit) { + return Prefetch(aURI, aReferrerInfo, aSource, aExplicit); +} + +NS_IMETHODIMP +nsPrefetchService::HasMoreElements(bool* aHasMore) { + *aHasMore = (mCurrentNodes.Length() || !mPrefetchQueue.empty()); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsIWebProgressListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchService::OnProgressChange(nsIWebProgress* aProgress, + nsIRequest* aRequest, + int32_t curSelfProgress, + int32_t maxSelfProgress, + int32_t curTotalProgress, + int32_t maxTotalProgress) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t progressStateFlags, + nsresult aStatus) { + if (progressStateFlags & STATE_IS_DOCUMENT) { + if (progressStateFlags & STATE_STOP) + StartPrefetching(); + else if (progressStateFlags & STATE_START) + StopPrefetching(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* location, + uint32_t aFlags) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aState) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsIObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + LOG(("nsPrefetchService::Observe [topic=%s]\n", aTopic)); + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + StopAll(); + mPrefetchDisabled = true; + } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + const nsCString converted = NS_ConvertUTF16toUTF8(aData); + const char* pref = converted.get(); + if (!strcmp(pref, PREFETCH_PREF)) { + if (Preferences::GetBool(PREFETCH_PREF, false)) { + if (mPrefetchDisabled) { + LOG(("enabling prefetching\n")); + mPrefetchDisabled = false; + AddProgressListener(); + } + } else { + if (!mPrefetchDisabled) { + LOG(("disabling prefetching\n")); + StopCurrentPrefetchsPreloads(false); + mPrefetchDisabled = true; + RemoveProgressListener(); + } + } + } else if (!strcmp(pref, PARALLELISM_PREF)) { + mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism); + if (mMaxParallelism < 1) { + mMaxParallelism = 1; + } + // If our parallelism has increased, go ahead and kick off enough + // prefetches to fill up our allowance. If we're now over our + // allowance, we'll just silently let some of them finish to get + // back below our limit. + while (((!mStopCount && mHaveProcessed) || mAggressive) && + !mPrefetchQueue.empty() && + mCurrentNodes.Length() < static_cast(mMaxParallelism)) { + ProcessNextPrefetchURI(); + } + } else if (!strcmp(pref, AGGRESSIVE_PREF)) { + mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false); + // in aggressive mode, start prefetching immediately + if (mAggressive) { + while (mStopCount && !mPrefetchQueue.empty() && + mCurrentNodes.Length() < + static_cast(mMaxParallelism)) { + ProcessNextPrefetchURI(); + } + } + } + } + + return NS_OK; +} + +// vim: ts=4 sw=2 expandtab diff --git a/uriloader/prefetch/nsPrefetchService.h b/uriloader/prefetch/nsPrefetchService.h new file mode 100644 index 0000000000..7b931ba52c --- /dev/null +++ b/uriloader/prefetch/nsPrefetchService.h @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 nsPrefetchService_h__ +#define nsPrefetchService_h__ + +#include "nsIObserver.h" +#include "nsIInterfaceRequestor.h" +#include "nsIChannelEventSink.h" +#include "nsIPrefetchService.h" +#include "nsIRedirectResultListener.h" +#include "nsIWebProgressListener.h" +#include "nsIStreamListener.h" +#include "nsIChannel.h" +#include "nsIURI.h" +#include "nsWeakReference.h" +#include "nsCOMPtr.h" +#include "mozilla/Attributes.h" +#include + +class nsPrefetchService; +class nsPrefetchNode; +class nsIReferrerInfo; + +//----------------------------------------------------------------------------- +// nsPrefetchService +//----------------------------------------------------------------------------- + +class nsPrefetchService final : public nsIPrefetchService, + public nsIWebProgressListener, + public nsIObserver, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPREFETCHSERVICE + NS_DECL_NSIWEBPROGRESSLISTENER + NS_DECL_NSIOBSERVER + + nsPrefetchService(); + + nsresult Init(); + void RemoveNodeAndMaybeStartNextPrefetchURI(nsPrefetchNode* aFinished); + void ProcessNextPrefetchURI(); + + void NotifyLoadRequested(nsPrefetchNode* node); + void NotifyLoadCompleted(nsPrefetchNode* node); + void DispatchEvent(nsPrefetchNode* node, bool aSuccess); + + private: + ~nsPrefetchService(); + + nsresult Prefetch(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, bool aExplicit); + + nsresult Preload(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, nsContentPolicyType aPolicyType); + + void AddProgressListener(); + void RemoveProgressListener(); + nsresult EnqueueURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, nsPrefetchNode** node); + void EmptyPrefetchQueue(); + + void StartPrefetching(); + void StopPrefetching(); + void StopCurrentPrefetchsPreloads(bool aPreload); + void StopAll(); + nsresult CheckURIScheme(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo); + + std::deque> mPrefetchQueue; + nsTArray> mCurrentNodes; + int32_t mMaxParallelism; + int32_t mStopCount; + bool mHaveProcessed; + bool mPrefetchDisabled; + + // In usual case prefetch does not start until all normal loads are done. + // Aggressive mode ignores normal loads and just start prefetch ASAP. + // It's mainly for testing purpose and discoraged for normal use; + // see https://bugzilla.mozilla.org/show_bug.cgi?id=1281415 for details. + bool mAggressive; +}; + +//----------------------------------------------------------------------------- +// nsPreFetchingNode +//----------------------------------------------------------------------------- + +class nsPrefetchNode final : public nsIStreamListener, + public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsIRedirectResultListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIREDIRECTRESULTLISTENER + + nsPrefetchNode(nsPrefetchService* aPrefetchService, nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, nsINode* aSource, + nsContentPolicyType aPolicyType, bool aPreload); + + nsresult OpenChannel(); + nsresult CancelChannel(nsresult error); + + nsCOMPtr mURI; + nsCOMPtr mReferrerInfo; + nsTArray mSources; + + // The policy type to be used for fetching the resource. + nsContentPolicyType mPolicyType; + // nsPrefetchNode is used for prefetching and preloading resource. + // mPreload is true if a resource is preloaded. Preloads and + // prefetches are fetched in different phases (during load and + // after a page load), therefore we need to distinguish them. + bool mPreload; + + private: + ~nsPrefetchNode() {} + + RefPtr mService; + nsCOMPtr mChannel; + nsCOMPtr mRedirectChannel; + int64_t mBytesRead; + bool mShouldFireLoadEvent; +}; + +#endif // !nsPrefetchService_h__ diff --git a/uriloader/preload/FetchPreloader.cpp b/uriloader/preload/FetchPreloader.cpp new file mode 100644 index 0000000000..6e848bbee8 --- /dev/null +++ b/uriloader/preload/FetchPreloader.cpp @@ -0,0 +1,353 @@ +/* -*- 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 "FetchPreloader.h" + +#include "mozilla/CORSMode.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/dom/Document.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "nsContentPolicyUtils.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIChildChannel.h" +#include "nsIClassOfService.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsITimedChannel.h" +#include "nsNetUtil.h" +#include "nsStringStream.h" +#include "nsIDocShell.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(FetchPreloader, nsIStreamListener, nsIRequestObserver) + +FetchPreloader::FetchPreloader() + : FetchPreloader(nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD) {} + +FetchPreloader::FetchPreloader(nsContentPolicyType aContentPolicyType) + : mContentPolicyType(aContentPolicyType) {} + +nsresult FetchPreloader::OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI, + const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, + dom::Document* aDocument, + uint64_t aEarlyHintPreloaderId) { + nsresult rv; + nsCOMPtr channel; + + auto notify = MakeScopeExit([&]() { + if (NS_FAILED(rv)) { + // Make sure we notify any elements when opening fails + // because of various technical or security reasons. + NotifyStart(channel); + // Using the non-channel overload of this method to make it work even + // before NotifyOpen has been called on this preload. We are not + // switching between channels, so it's safe to do so. + NotifyStop(rv); + } + }); + + nsCOMPtr loadGroup = aDocument->GetDocumentLoadGroup(); + nsCOMPtr window = aDocument->GetWindow(); + nsCOMPtr prompter; + if (window) { + nsIDocShell* docshell = window->GetDocShell(); + prompter = do_QueryInterface(docshell); + } + + rv = CreateChannel(getter_AddRefs(channel), aURI, aCORSMode, aReferrerPolicy, + aDocument, loadGroup, prompter, aEarlyHintPreloaderId); + NS_ENSURE_SUCCESS(rv, rv); + + // Doing this now so that we have the channel and tainting set on it properly + // to notify the proper event (load or error) on the associated preload tags + // when the CSP check fails. + rv = CheckContentPolicy(aURI, aDocument); + if (NS_FAILED(rv)) { + return rv; + } + + PrioritizeAsPreload(channel); + AddLoadBackgroundFlag(channel); + + NotifyOpen(aKey, channel, aDocument, true); + + if (aEarlyHintPreloaderId) { + nsCOMPtr channelInternal = + do_QueryInterface(channel); + NS_ENSURE_TRUE(channelInternal != nullptr, NS_ERROR_FAILURE); + + rv = channelInternal->SetEarlyHintPreloaderId(aEarlyHintPreloaderId); + NS_ENSURE_SUCCESS(rv, rv); + } + return mAsyncConsumeResult = rv = channel->AsyncOpen(this); +} + +nsresult FetchPreloader::CreateChannel( + nsIChannel** aChannel, nsIURI* aURI, const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, dom::Document* aDocument, + nsILoadGroup* aLoadGroup, nsIInterfaceRequestor* aCallbacks, + uint64_t aEarlyHintPreloaderId) { + nsresult rv; + + nsSecurityFlags securityFlags = + nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS); + + nsCOMPtr channel; + rv = NS_NewChannelWithTriggeringPrincipal( + getter_AddRefs(channel), aURI, aDocument, aDocument->NodePrincipal(), + securityFlags, nsIContentPolicy::TYPE_FETCH, nullptr, aLoadGroup, + aCallbacks); + if (NS_FAILED(rv)) { + return rv; + } + + if (nsCOMPtr httpChannel = do_QueryInterface(channel)) { + nsCOMPtr referrerInfo = new dom::ReferrerInfo( + aDocument->GetDocumentURIAsReferrer(), aReferrerPolicy); + rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (nsCOMPtr timedChannel = do_QueryInterface(channel)) { + if (aEarlyHintPreloaderId) { + timedChannel->SetInitiatorType(u"early-hints"_ns); + } else { + timedChannel->SetInitiatorType(u"link"_ns); + } + } + + channel.forget(aChannel); + return NS_OK; +} + +nsresult FetchPreloader::CheckContentPolicy(nsIURI* aURI, + dom::Document* aDocument) { + if (!aDocument) { + return NS_OK; + } + + nsCOMPtr secCheckLoadInfo = new net::LoadInfo( + aDocument->NodePrincipal(), aDocument->NodePrincipal(), aDocument, + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, mContentPolicyType); + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + nsresult rv = + NS_CheckContentLoadPolicy(aURI, secCheckLoadInfo, ""_ns, &shouldLoad, + nsContentUtils::GetContentPolicy()); + if (NS_SUCCEEDED(rv) && NS_CP_ACCEPTED(shouldLoad)) { + return NS_OK; + } + + return NS_ERROR_CONTENT_BLOCKED; +} + +// PreloaderBase + +nsresult FetchPreloader::AsyncConsume(nsIStreamListener* aListener) { + if (NS_FAILED(mAsyncConsumeResult)) { + // Already being consumed or failed to open. + return mAsyncConsumeResult; + } + + // Prevent duplicate calls. + mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE; + + if (!mConsumeListener) { + // Called before we are getting response from the channel. + mConsumeListener = aListener; + } else { + // Channel already started, push cached calls to this listener. + // Can't be anything else than the `Cache`, hence a safe static_cast. + Cache* cache = static_cast(mConsumeListener.get()); + cache->AsyncConsume(aListener); + } + + return NS_OK; +} + +// static +void FetchPreloader::PrioritizeAsPreload(nsIChannel* aChannel) { + if (nsCOMPtr cos = do_QueryInterface(aChannel)) { + cos->AddClassFlags(nsIClassOfService::Unblocked); + } +} + +void FetchPreloader::PrioritizeAsPreload() { PrioritizeAsPreload(Channel()); } + +// nsIRequestObserver + nsIStreamListener + +NS_IMETHODIMP FetchPreloader::OnStartRequest(nsIRequest* request) { + NotifyStart(request); + + if (!mConsumeListener) { + // AsyncConsume not called yet. + mConsumeListener = new Cache(); + } + + return mConsumeListener->OnStartRequest(request); +} + +NS_IMETHODIMP FetchPreloader::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, uint32_t count) { + return mConsumeListener->OnDataAvailable(request, input, offset, count); +} + +NS_IMETHODIMP FetchPreloader::OnStopRequest(nsIRequest* request, + nsresult status) { + mConsumeListener->OnStopRequest(request, status); + + // We want 404 or other types of server responses to be treated as 'error'. + if (nsCOMPtr httpChannel = do_QueryInterface(request)) { + uint32_t responseStatus = 0; + Unused << httpChannel->GetResponseStatus(&responseStatus); + if (responseStatus / 100 != 2) { + status = NS_ERROR_FAILURE; + } + } + + // Fetch preloader wants to keep the channel around so that consumers like XHR + // can access it even after the preload is done. + nsCOMPtr channel = mChannel; + NotifyStop(request, status); + mChannel.swap(channel); + return NS_OK; +} + +// FetchPreloader::Cache + +NS_IMPL_ISUPPORTS(FetchPreloader::Cache, nsIStreamListener, nsIRequestObserver) + +NS_IMETHODIMP FetchPreloader::Cache::OnStartRequest(nsIRequest* request) { + mRequest = request; + + if (mFinalListener) { + return mFinalListener->OnStartRequest(mRequest); + } + + mCalls.AppendElement(Call{VariantIndex<0>{}, StartRequest{}}); + return NS_OK; +} + +NS_IMETHODIMP FetchPreloader::Cache::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, + uint32_t count) { + if (mFinalListener) { + return mFinalListener->OnDataAvailable(mRequest, input, offset, count); + } + + DataAvailable data; + if (!data.mData.SetLength(count, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t read; + nsresult rv = input->Read(data.mData.BeginWriting(), count, &read); + if (NS_FAILED(rv)) { + return rv; + } + + mCalls.AppendElement(Call{VariantIndex<1>{}, std::move(data)}); + return NS_OK; +} + +NS_IMETHODIMP FetchPreloader::Cache::OnStopRequest(nsIRequest* request, + nsresult status) { + if (mFinalListener) { + return mFinalListener->OnStopRequest(mRequest, status); + } + + mCalls.AppendElement(Call{VariantIndex<2>{}, StopRequest{status}}); + return NS_OK; +} + +void FetchPreloader::Cache::AsyncConsume(nsIStreamListener* aListener) { + // Must dispatch for two reasons: + // 1. This is called directly from FetchLoader::AsyncConsume, which must + // behave the same way as nsIChannel::AsyncOpen. + // 2. In case there are already stream listener events scheduled on the main + // thread we preserve the order - those will still end up in Cache. + + // * The `Cache` object is fully main thread only for now, doesn't support + // retargeting, but it can be improved to allow it. + + nsCOMPtr listener(aListener); + NS_DispatchToMainThread(NewRunnableMethod>( + "FetchPreloader::Cache::Consume", this, &FetchPreloader::Cache::Consume, + listener)); +} + +void FetchPreloader::Cache::Consume(nsCOMPtr aListener) { + MOZ_ASSERT(!mFinalListener, "Duplicate call"); + + mFinalListener = std::move(aListener); + + // Status of the channel read after each call. + nsresult status = NS_OK; + nsCOMPtr channel(do_QueryInterface(mRequest)); + + RefPtr self(this); + for (auto& call : mCalls) { + nsresult rv = call.match( + [&](const StartRequest& startRequest) mutable { + return self->mFinalListener->OnStartRequest(self->mRequest); + }, + [&](const DataAvailable& dataAvailable) mutable { + if (NS_FAILED(status)) { + // Channel has been cancelled during this mCalls loop. + return NS_OK; + } + + nsCOMPtr input; + rv = NS_NewCStringInputStream(getter_AddRefs(input), + dataAvailable.mData); + if (NS_FAILED(rv)) { + return rv; + } + + return self->mFinalListener->OnDataAvailable( + self->mRequest, input, 0, dataAvailable.mData.Length()); + }, + [&](const StopRequest& stopRequest) { + // First cancellation overrides mStatus in nsHttpChannel. + nsresult stopStatus = + NS_FAILED(status) ? status : stopRequest.mStatus; + self->mFinalListener->OnStopRequest(self->mRequest, stopStatus); + self->mFinalListener = nullptr; + self->mRequest = nullptr; + return NS_OK; + }); + + if (!mRequest) { + // We are done! + break; + } + + bool isCancelled = false; + Unused << channel->GetCanceled(&isCancelled); + if (isCancelled) { + mRequest->GetStatus(&status); + } else if (NS_FAILED(rv)) { + status = rv; + mRequest->Cancel(status); + } + } + + mCalls.Clear(); +} + +} // namespace mozilla diff --git a/uriloader/preload/FetchPreloader.h b/uriloader/preload/FetchPreloader.h new file mode 100644 index 0000000000..b6b667fad3 --- /dev/null +++ b/uriloader/preload/FetchPreloader.h @@ -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/. */ + +#ifndef FetchPreloader_h_ +#define FetchPreloader_h_ + +#include "mozilla/PreloaderBase.h" +#include "mozilla/Variant.h" +#include "nsCOMPtr.h" +#include "nsIAsyncOutputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsIContentPolicy.h" +#include "nsIStreamListener.h" + +class nsIChannel; +class nsILoadGroup; +class nsIInterfaceRequestor; + +namespace mozilla { + +namespace dom { +enum class ReferrerPolicy : uint8_t; +} + +class FetchPreloader : public PreloaderBase, public nsIStreamListener { + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + FetchPreloader(); + nsresult OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI, + const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, + dom::Document* aDocument, + uint64_t aEarlyHintPreloaderId); + + // PreloaderBase + nsresult AsyncConsume(nsIStreamListener* aListener) override; + static void PrioritizeAsPreload(nsIChannel* aChannel); + void PrioritizeAsPreload() override; + + protected: + explicit FetchPreloader(nsContentPolicyType aContentPolicyType); + virtual ~FetchPreloader() = default; + + // Create and setup the channel with necessary security properties. This is + // overridable by subclasses to allow different initial conditions. + virtual nsresult CreateChannel(nsIChannel** aChannel, nsIURI* aURI, + const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, + dom::Document* aDocument, + nsILoadGroup* aLoadGroup, + nsIInterfaceRequestor* aCallbacks, + uint64_t aEarlyHintPreloaderId); + + private: + nsresult CheckContentPolicy(nsIURI* aURI, dom::Document* aDocument); + + class Cache final : public nsIStreamListener { + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + void AsyncConsume(nsIStreamListener* aListener); + void Consume(nsCOMPtr aListener); + + private: + virtual ~Cache() = default; + + struct StartRequest {}; + struct DataAvailable { + nsCString mData; + }; + struct StopRequest { + nsresult mStatus; + }; + + typedef Variant Call; + nsCOMPtr mRequest; + nsCOMPtr mFinalListener; + nsTArray mCalls; + }; + + // The listener passed to AsyncConsume in case we haven't started getting the + // data from the channel yet. + nsCOMPtr mConsumeListener; + + // Returned by AsyncConsume when a failure. This remembers the result of + // opening the channel and prevents duplicate consumation. + nsresult mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE; + + // The CP type to check against. Derived classes have to override call to CSP + // constructor of this class. + nsContentPolicyType mContentPolicyType; +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/preload/PreloadHashKey.cpp b/uriloader/preload/PreloadHashKey.cpp new file mode 100644 index 0000000000..989bb16949 --- /dev/null +++ b/uriloader/preload/PreloadHashKey.cpp @@ -0,0 +1,213 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "PreloadHashKey.h" + +#include "mozilla/dom/Element.h" // StringToCORSMode +#include "mozilla/css/SheetLoadData.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "nsIPrincipal.h" +#include "nsIReferrerInfo.h" + +namespace mozilla { + +PreloadHashKey::PreloadHashKey(const nsIURI* aKey, ResourceType aAs) + : nsURIHashKey(aKey), mAs(aAs) {} + +PreloadHashKey::PreloadHashKey(const PreloadHashKey* aKey) + : nsURIHashKey(aKey->mKey) { + *this = *aKey; +} + +PreloadHashKey::PreloadHashKey(PreloadHashKey&& aToMove) + : nsURIHashKey(std::move(aToMove)) { + mAs = std::move(aToMove.mAs); + mCORSMode = std::move(aToMove.mCORSMode); + mPrincipal = std::move(aToMove.mPrincipal); + + switch (mAs) { + case ResourceType::SCRIPT: + mScript = std::move(aToMove.mScript); + break; + case ResourceType::STYLE: + mStyle = std::move(aToMove.mStyle); + break; + case ResourceType::IMAGE: + break; + case ResourceType::FONT: + break; + case ResourceType::FETCH: + break; + case ResourceType::NONE: + break; + } +} + +PreloadHashKey& PreloadHashKey::operator=(const PreloadHashKey& aOther) { + MOZ_ASSERT(mAs == ResourceType::NONE || aOther.mAs == ResourceType::NONE, + "Assigning more than once, only reset is allowed"); + + nsURIHashKey::operator=(aOther); + + mAs = aOther.mAs; + mCORSMode = aOther.mCORSMode; + mPrincipal = aOther.mPrincipal; + + switch (mAs) { + case ResourceType::SCRIPT: + mScript = aOther.mScript; + break; + case ResourceType::STYLE: + mStyle = aOther.mStyle; + break; + case ResourceType::IMAGE: + break; + case ResourceType::FONT: + break; + case ResourceType::FETCH: + break; + case ResourceType::NONE: + break; + } + + return *this; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsScript( + nsIURI* aURI, CORSMode aCORSMode, JS::loader::ScriptKind aScriptKind) { + PreloadHashKey key(aURI, ResourceType::SCRIPT); + key.mCORSMode = aCORSMode; + + key.mScript.mScriptKind = aScriptKind; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsScript(nsIURI* aURI, + const nsAString& aCrossOrigin, + const nsAString& aType) { + JS::loader::ScriptKind scriptKind = JS::loader::ScriptKind::eClassic; + if (aType.LowerCaseEqualsASCII("module")) { + scriptKind = JS::loader::ScriptKind::eModule; + } + CORSMode crossOrigin = dom::Element::StringToCORSMode(aCrossOrigin); + return CreateAsScript(aURI, crossOrigin, scriptKind); +} + +// static +PreloadHashKey PreloadHashKey::CreateAsStyle( + nsIURI* aURI, nsIPrincipal* aPrincipal, CORSMode aCORSMode, + css::SheetParsingMode aParsingMode) { + PreloadHashKey key(aURI, ResourceType::STYLE); + key.mCORSMode = aCORSMode; + key.mPrincipal = aPrincipal; + + key.mStyle.mParsingMode = aParsingMode; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsStyle( + css::SheetLoadData& aSheetLoadData) { + return CreateAsStyle(aSheetLoadData.mURI, aSheetLoadData.mTriggeringPrincipal, + aSheetLoadData.mSheet->GetCORSMode(), + aSheetLoadData.mSheet->ParsingMode()); +} + +// static +PreloadHashKey PreloadHashKey::CreateAsImage(nsIURI* aURI, + nsIPrincipal* aPrincipal, + CORSMode aCORSMode) { + PreloadHashKey key(aURI, ResourceType::IMAGE); + key.mCORSMode = aCORSMode; + key.mPrincipal = aPrincipal; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsFetch(nsIURI* aURI, CORSMode aCORSMode) { + PreloadHashKey key(aURI, ResourceType::FETCH); + key.mCORSMode = aCORSMode; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsFetch(nsIURI* aURI, + const nsAString& aCrossOrigin) { + return CreateAsFetch(aURI, dom::Element::StringToCORSMode(aCrossOrigin)); +} + +PreloadHashKey PreloadHashKey::CreateAsFont(nsIURI* aURI, CORSMode aCORSMode) { + PreloadHashKey key(aURI, ResourceType::FONT); + key.mCORSMode = aCORSMode; + + return key; +} + +bool PreloadHashKey::KeyEquals(KeyTypePointer aOther) const { + if (mAs != aOther->mAs || mCORSMode != aOther->mCORSMode) { + return false; + } + + if (!mPrincipal != !aOther->mPrincipal) { + // One or the other has a principal, but not both... not equal + return false; + } + + if (mPrincipal && !mPrincipal->Equals(aOther->mPrincipal)) { + return false; + } + + if (!nsURIHashKey::KeyEquals( + static_cast(aOther)->GetKey())) { + return false; + } + + switch (mAs) { + case ResourceType::SCRIPT: + if (mScript.mScriptKind != aOther->mScript.mScriptKind) { + return false; + } + break; + case ResourceType::STYLE: { + if (mStyle.mParsingMode != aOther->mStyle.mParsingMode) { + return false; + } + break; + } + case ResourceType::IMAGE: + // No more checks needed. The image cache key consists of the document + // (which we are scoped into), origin attributes (which we compare as part + // of the principal check) and the URL. imgLoader::ValidateEntry compares + // CORS, referrer info and principal, which we do by default. + break; + case ResourceType::FONT: + break; + case ResourceType::FETCH: + // No more checks needed. + break; + case ResourceType::NONE: + break; + } + + return true; +} + +// static +PLDHashNumber PreloadHashKey::HashKey(KeyTypePointer aKey) { + PLDHashNumber hash = nsURIHashKey::HashKey(aKey->mKey); + + // Enough to use the common attributes + hash = AddToHash(hash, static_cast(aKey->mAs)); + hash = AddToHash(hash, static_cast(aKey->mCORSMode)); + + return hash; +} + +} // namespace mozilla diff --git a/uriloader/preload/PreloadHashKey.h b/uriloader/preload/PreloadHashKey.h new file mode 100644 index 0000000000..8db49c1b18 --- /dev/null +++ b/uriloader/preload/PreloadHashKey.h @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 PreloadHashKey_h__ +#define PreloadHashKey_h__ + +#include "mozilla/CORSMode.h" +#include "mozilla/css/SheetParsingMode.h" +#include "js/loader/ScriptKind.h" +#include "nsURIHashKey.h" + +class nsIPrincipal; + +namespace JS::loader { +enum class ScriptKind; +} + +namespace mozilla { + +namespace css { +class SheetLoadData; +} + +/** + * This key is used for coalescing and lookup of preloading or regular + * speculative loads. It consists of: + * - the resource type, which is the value of the 'as' attribute + * - the URI of the resource + * - set of attributes that is common to all resource types + * - resource-type-specific attributes that we use to distinguish loads that has + * to be treated separately, some of these attributes may remain at their + * default values + */ +class PreloadHashKey : public nsURIHashKey { + public: + enum class ResourceType : uint8_t { NONE, SCRIPT, STYLE, IMAGE, FONT, FETCH }; + + using KeyType = const PreloadHashKey&; + using KeyTypePointer = const PreloadHashKey*; + + PreloadHashKey() = default; + PreloadHashKey(const nsIURI* aKey, ResourceType aAs); + explicit PreloadHashKey(const PreloadHashKey* aKey); + PreloadHashKey(PreloadHashKey&& aToMove); + + PreloadHashKey& operator=(const PreloadHashKey& aOther); + + // Construct key for "script" + static PreloadHashKey CreateAsScript(nsIURI* aURI, CORSMode aCORSMode, + JS::loader::ScriptKind aScriptKind); + static PreloadHashKey CreateAsScript(nsIURI* aURI, + const nsAString& aCrossOrigin, + const nsAString& aType); + + // Construct key for "style" + static PreloadHashKey CreateAsStyle(nsIURI* aURI, nsIPrincipal* aPrincipal, + CORSMode aCORSMode, + css::SheetParsingMode aParsingMode); + static PreloadHashKey CreateAsStyle(css::SheetLoadData&); + + // Construct key for "image" + static PreloadHashKey CreateAsImage(nsIURI* aURI, nsIPrincipal* aPrincipal, + CORSMode aCORSMode); + + // Construct key for "fetch" + static PreloadHashKey CreateAsFetch(nsIURI* aURI, CORSMode aCORSMode); + static PreloadHashKey CreateAsFetch(nsIURI* aURI, + const nsAString& aCrossOrigin); + + // Construct key for "font" + static PreloadHashKey CreateAsFont(nsIURI* aURI, CORSMode aCORSMode); + + KeyType GetKey() const { return *this; } + KeyTypePointer GetKeyPointer() const { return this; } + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + + bool KeyEquals(KeyTypePointer aOther) const; + static PLDHashNumber HashKey(KeyTypePointer aKey); + ResourceType As() const { return mAs; } + +#ifdef MOZILLA_INTERNAL_API + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + // Bug 1627752 + return 0; + } +#endif + + enum { ALLOW_MEMMOVE = true }; + + private: + // Attributes common to all resource types + ResourceType mAs = ResourceType::NONE; + + CORSMode mCORSMode = CORS_NONE; + nsCOMPtr mPrincipal; + + struct { + JS::loader::ScriptKind mScriptKind = JS::loader::ScriptKind::eClassic; + } mScript; + + struct { + css::SheetParsingMode mParsingMode = css::eAuthorSheetFeatures; + } mStyle; +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/preload/PreloadService.cpp b/uriloader/preload/PreloadService.cpp new file mode 100644 index 0000000000..6766dc5799 --- /dev/null +++ b/uriloader/preload/PreloadService.cpp @@ -0,0 +1,296 @@ +/* -*- 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 "PreloadService.h" + +#include "FetchPreloader.h" +#include "PreloaderBase.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/HTMLLinkElement.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/Encoding.h" +#include "mozilla/FontPreloader.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsNetUtil.h" + +namespace mozilla { + +PreloadService::PreloadService(dom::Document* aDoc) : mDocument(aDoc) {} +PreloadService::~PreloadService() = default; + +bool PreloadService::RegisterPreload(const PreloadHashKey& aKey, + PreloaderBase* aPreload) { + return mPreloads.WithEntryHandle(aKey, [&](auto&& lookup) { + if (lookup) { + lookup.Data() = aPreload; + return true; + } + lookup.Insert(aPreload); + return false; + }); +} + +void PreloadService::DeregisterPreload(const PreloadHashKey& aKey) { + mPreloads.Remove(aKey); +} + +void PreloadService::ClearAllPreloads() { mPreloads.Clear(); } + +bool PreloadService::PreloadExists(const PreloadHashKey& aKey) { + return mPreloads.Contains(aKey); +} + +already_AddRefed PreloadService::LookupPreload( + const PreloadHashKey& aKey) const { + return mPreloads.Get(aKey); +} + +already_AddRefed PreloadService::GetPreloadURI(const nsAString& aURL) { + nsIURI* base = BaseURIForPreload(); + auto encoding = mDocument->GetDocumentCharacterSet(); + + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, encoding, base); + if (NS_FAILED(rv)) { + return nullptr; + } + + return uri.forget(); +} + +already_AddRefed PreloadService::PreloadLinkElement( + dom::HTMLLinkElement* aLinkElement, nsContentPolicyType aPolicyType) { + if (aPolicyType == nsIContentPolicy::TYPE_INVALID) { + MOZ_ASSERT_UNREACHABLE("Caller should check"); + return nullptr; + } + + if (!StaticPrefs::network_preload()) { + return nullptr; + } + + nsAutoString as, charset, crossOrigin, integrity, referrerPolicy, rel, srcset, + sizes, type, url; + + nsCOMPtr uri = aLinkElement->GetURI(); + aLinkElement->GetCharset(charset); + aLinkElement->GetImageSrcset(srcset); + aLinkElement->GetImageSizes(sizes); + aLinkElement->GetHref(url); + aLinkElement->GetCrossOrigin(crossOrigin); + aLinkElement->GetIntegrity(integrity); + aLinkElement->GetReferrerPolicy(referrerPolicy); + aLinkElement->GetRel(rel); + + if (rel.LowerCaseEqualsASCII("modulepreload")) { + as = u"script"_ns; + type = u"module"_ns; + } else { + aLinkElement->GetAs(as); + aLinkElement->GetType(type); + } + + auto result = PreloadOrCoalesce(uri, url, aPolicyType, as, type, charset, + srcset, sizes, integrity, crossOrigin, + referrerPolicy, /* aFromHeader = */ false, 0); + + if (!result.mPreloader) { + NotifyNodeEvent(aLinkElement, result.mAlreadyComplete); + return nullptr; + } + + result.mPreloader->AddLinkPreloadNode(aLinkElement); + return result.mPreloader.forget(); +} + +void PreloadService::PreloadLinkHeader( + nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType, + const nsAString& aAs, const nsAString& aType, const nsAString& aIntegrity, + const nsAString& aSrcset, const nsAString& aSizes, const nsAString& aCORS, + const nsAString& aReferrerPolicy, uint64_t aEarlyHintPreloaderId) { + if (aPolicyType == nsIContentPolicy::TYPE_INVALID) { + MOZ_ASSERT_UNREACHABLE("Caller should check"); + return; + } + + if (!StaticPrefs::network_preload()) { + return; + } + + PreloadOrCoalesce(aURI, aURL, aPolicyType, aAs, aType, u""_ns, aSrcset, + aSizes, aIntegrity, aCORS, aReferrerPolicy, + /* aFromHeader = */ true, aEarlyHintPreloaderId); +} + +PreloadService::PreloadOrCoalesceResult PreloadService::PreloadOrCoalesce( + nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType, + const nsAString& aAs, const nsAString& aType, const nsAString& aCharset, + const nsAString& aSrcset, const nsAString& aSizes, + const nsAString& aIntegrity, const nsAString& aCORS, + const nsAString& aReferrerPolicy, bool aFromHeader, + uint64_t aEarlyHintPreloaderId) { + if (!aURI) { + MOZ_ASSERT_UNREACHABLE("Should not pass null nsIURI"); + return {nullptr, false}; + } + + bool isImgSet = false; + PreloadHashKey preloadKey; + nsCOMPtr uri = aURI; + + if (aAs.LowerCaseEqualsASCII("script")) { + preloadKey = PreloadHashKey::CreateAsScript(uri, aCORS, aType); + } else if (aAs.LowerCaseEqualsASCII("style")) { + preloadKey = PreloadHashKey::CreateAsStyle( + uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS), + css::eAuthorSheetFeatures /* see Loader::LoadSheet */); + } else if (aAs.LowerCaseEqualsASCII("image")) { + uri = mDocument->ResolvePreloadImage(BaseURIForPreload(), aURL, aSrcset, + aSizes, &isImgSet); + if (!uri) { + return {nullptr, false}; + } + + preloadKey = PreloadHashKey::CreateAsImage( + uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS)); + } else if (aAs.LowerCaseEqualsASCII("font")) { + preloadKey = PreloadHashKey::CreateAsFont( + uri, dom::Element::StringToCORSMode(aCORS)); + } else if (aAs.LowerCaseEqualsASCII("fetch")) { + preloadKey = PreloadHashKey::CreateAsFetch( + uri, dom::Element::StringToCORSMode(aCORS)); + } else { + return {nullptr, false}; + } + + if (RefPtr preload = LookupPreload(preloadKey)) { + return {std::move(preload), false}; + } + + if (aAs.LowerCaseEqualsASCII("script")) { + PreloadScript(uri, aType, aCharset, aCORS, aReferrerPolicy, aIntegrity, + true /* isInHead - TODO */, aEarlyHintPreloaderId); + } else if (aAs.LowerCaseEqualsASCII("style")) { + auto status = mDocument->PreloadStyle( + aURI, Encoding::ForLabel(aCharset), aCORS, + PreloadReferrerPolicy(aReferrerPolicy), aIntegrity, + aFromHeader ? css::StylePreloadKind::FromLinkRelPreloadHeader + : css::StylePreloadKind::FromLinkRelPreloadElement, + aEarlyHintPreloaderId); + switch (status) { + case dom::SheetPreloadStatus::AlreadyComplete: + return {nullptr, /* already_complete = */ true}; + case dom::SheetPreloadStatus::Errored: + case dom::SheetPreloadStatus::InProgress: + break; + } + } else if (aAs.LowerCaseEqualsASCII("image")) { + PreloadImage(uri, aCORS, aReferrerPolicy, isImgSet, aEarlyHintPreloaderId); + } else if (aAs.LowerCaseEqualsASCII("font")) { + PreloadFont(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId); + } else if (aAs.LowerCaseEqualsASCII("fetch")) { + PreloadFetch(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId); + } + + RefPtr preload = LookupPreload(preloadKey); + if (preload && aEarlyHintPreloaderId) { + preload->SetForEarlyHints(); + } + + return {preload, false}; +} + +void PreloadService::PreloadScript(nsIURI* aURI, const nsAString& aType, + const nsAString& aCharset, + const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + const nsAString& aIntegrity, + bool aScriptFromHead, + uint64_t aEarlyHintPreloaderId) { + mDocument->ScriptLoader()->PreloadURI( + aURI, aCharset, aType, aCrossOrigin, aIntegrity, aScriptFromHead, false, + false, false, true, PreloadReferrerPolicy(aReferrerPolicy), + aEarlyHintPreloaderId); +} + +void PreloadService::PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aImageReferrerPolicy, + bool aIsImgSet, + uint64_t aEarlyHintPreloaderId) { + mDocument->PreLoadImage(aURI, aCrossOrigin, + PreloadReferrerPolicy(aImageReferrerPolicy), + aIsImgSet, true, aEarlyHintPreloaderId); +} + +void PreloadService::PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + uint64_t aEarlyHintPreloaderId) { + CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin); + auto key = PreloadHashKey::CreateAsFont(aURI, cors); + + if (PreloadExists(key)) { + return; + } + + RefPtr preloader = new FontPreloader(); + dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy); + preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument, + aEarlyHintPreloaderId); +} + +void PreloadService::PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + uint64_t aEarlyHintPreloaderId) { + CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin); + auto key = PreloadHashKey::CreateAsFetch(aURI, cors); + + if (PreloadExists(key)) { + return; + } + + RefPtr preloader = new FetchPreloader(); + dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy); + preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument, + aEarlyHintPreloaderId); +} + +// static +void PreloadService::NotifyNodeEvent(nsINode* aNode, bool aSuccess) { + if (!aNode->IsInComposedDoc()) { + return; + } + + // We don't dispatch synchronously since |node| might be in a DocGroup + // that we're not allowed to touch. (Our network request happens in the + // DocGroup of one of the mSources nodes--not necessarily this one). + + RefPtr dispatcher = new AsyncEventDispatcher( + aNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo); + + dispatcher->RequireNodeInDocument(); + dispatcher->PostDOMEvent(); +} + +dom::ReferrerPolicy PreloadService::PreloadReferrerPolicy( + const nsAString& aReferrerPolicy) { + dom::ReferrerPolicy referrerPolicy = + dom::ReferrerInfo::ReferrerPolicyAttributeFromString(aReferrerPolicy); + if (referrerPolicy == dom::ReferrerPolicy::_empty) { + referrerPolicy = mDocument->GetPreloadReferrerInfo()->ReferrerPolicy(); + } + + return referrerPolicy; +} + +nsIURI* PreloadService::BaseURIForPreload() { + nsIURI* documentURI = mDocument->GetDocumentURI(); + nsIURI* documentBaseURI = mDocument->GetDocBaseURI(); + return (documentURI == documentBaseURI) + ? (mSpeculationBaseURI ? mSpeculationBaseURI.get() : documentURI) + : documentBaseURI; +} + +} // namespace mozilla diff --git a/uriloader/preload/PreloadService.h b/uriloader/preload/PreloadService.h new file mode 100644 index 0000000000..d785fd0ae5 --- /dev/null +++ b/uriloader/preload/PreloadService.h @@ -0,0 +1,135 @@ +/* -*- 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 PreloadService_h__ +#define PreloadService_h__ + +#include "nsIContentPolicy.h" +#include "nsIURI.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/PreloadHashKey.h" + +class nsINode; + +namespace mozilla { + +class PreloaderBase; + +namespace dom { + +class HTMLLinkElement; +class Document; +enum class ReferrerPolicy : uint8_t; + +} // namespace dom + +/** + * Intended to scope preloads and speculative loads under one roof. This class + * is intended to be a member of dom::Document. Provides registration of + * speculative loads via a `key` which is defined to consist of the URL, + * resource type, and resource-specific attributes that are further + * distinguishing loads with otherwise same type and url. + */ +class PreloadService { + public: + explicit PreloadService(dom::Document*); + ~PreloadService(); + + // Called by resource loaders to register a running resource load. This is + // called for a speculative load when it's started the first time, being it + // either regular speculative load or a preload. + // + // Returns false and does nothing if a preload is already registered under + // this key, true otherwise. + bool RegisterPreload(const PreloadHashKey& aKey, PreloaderBase* aPreload); + + // Called when the load is about to be cancelled. Exact behavior is to be + // determined yet. + void DeregisterPreload(const PreloadHashKey& aKey); + + // Called when the scope is to go away. + void ClearAllPreloads(); + + // True when there is a preload registered under the key. + bool PreloadExists(const PreloadHashKey& aKey); + + // Returns an existing preload under the key or null, when there is none + // registered under the key. + already_AddRefed LookupPreload( + const PreloadHashKey& aKey) const; + + void SetSpeculationBase(nsIURI* aURI) { mSpeculationBaseURI = aURI; } + already_AddRefed GetPreloadURI(const nsAString& aURL); + + already_AddRefed PreloadLinkElement( + dom::HTMLLinkElement* aLink, nsContentPolicyType aPolicyType); + + // a non-zero aEarlyHintPreloaderId tells this service that a preload for this + // link was started by the EarlyHintPreloader and the preloaders should + // connect back by setting earlyHintPreloaderId in nsIChannelInternal before + // AsyncOpen. + void PreloadLinkHeader(nsIURI* aURI, const nsAString& aURL, + nsContentPolicyType aPolicyType, const nsAString& aAs, + const nsAString& aType, const nsAString& aIntegrity, + const nsAString& aSrcset, const nsAString& aSizes, + const nsAString& aCORS, + const nsAString& aReferrerPolicy, + uint64_t aEarlyHintPreloaderId); + + void PreloadScript(nsIURI* aURI, const nsAString& aType, + const nsAString& aCharset, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + const nsAString& aIntegrity, bool aScriptFromHead, + uint64_t aEarlyHintPreloaderId); + + void PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aImageReferrerPolicy, bool aIsImgSet, + uint64_t aEarlyHintPreloaderId); + + void PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + uint64_t aEarlyHintPreloaderId); + + void PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + uint64_t aEarlyHintPreloaderId); + + static void NotifyNodeEvent(nsINode* aNode, bool aSuccess); + + void SetEarlyHintUsed() { mEarlyHintUsed = true; } + bool GetEarlyHintUsed() const { return mEarlyHintUsed; } + + private: + dom::ReferrerPolicy PreloadReferrerPolicy(const nsAString& aReferrerPolicy); + nsIURI* BaseURIForPreload(); + + struct PreloadOrCoalesceResult { + RefPtr mPreloader; + bool mAlreadyComplete; + }; + + PreloadOrCoalesceResult PreloadOrCoalesce( + nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType, + const nsAString& aAs, const nsAString& aType, const nsAString& aCharset, + const nsAString& aSrcset, const nsAString& aSizes, + const nsAString& aIntegrity, const nsAString& aCORS, + const nsAString& aReferrerPolicy, bool aFromHeader, + uint64_t aEarlyHintPreloaderId); + + private: + nsRefPtrHashtable mPreloads; + + // Raw pointer only, we are intended to be a direct member of dom::Document + dom::Document* mDocument; + + // Set by `nsHtml5TreeOpExecutor::SetSpeculationBase`. + nsCOMPtr mSpeculationBaseURI; + + bool mEarlyHintUsed = false; +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/preload/PreloaderBase.cpp b/uriloader/preload/PreloaderBase.cpp new file mode 100644 index 0000000000..2c3d4fb60b --- /dev/null +++ b/uriloader/preload/PreloaderBase.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 "PreloaderBase.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/Telemetry.h" +#include "nsContentUtils.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIHttpChannel.h" +#include "nsILoadGroup.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIRedirectResultListener.h" +#include "nsNetUtil.h" + +// Change this if we want to cancel and remove the associated preload on removal +// of all tags from the tree. +constexpr static bool kCancelAndRemovePreloadOnZeroReferences = false; + +namespace mozilla { + +PreloaderBase::UsageTimer::UsageTimer(PreloaderBase* aPreload, + dom::Document* aDocument) + : mDocument(aDocument), mPreload(aPreload) {} + +class PreloaderBase::RedirectSink final : public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsIRedirectResultListener { + RedirectSink() = delete; + virtual ~RedirectSink(); + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIREDIRECTRESULTLISTENER + + RedirectSink(PreloaderBase* aPreloader, nsIInterfaceRequestor* aCallbacks); + + private: + MainThreadWeakPtr mPreloader; + nsCOMPtr mCallbacks; + nsCOMPtr mRedirectChannel; +}; + +PreloaderBase::RedirectSink::RedirectSink(PreloaderBase* aPreloader, + nsIInterfaceRequestor* aCallbacks) + : mPreloader(aPreloader), mCallbacks(aCallbacks) {} + +PreloaderBase::RedirectSink::~RedirectSink() = default; + +NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink, nsIInterfaceRequestor, + nsIChannelEventSink, nsIRedirectResultListener) + +NS_IMETHODIMP PreloaderBase::RedirectSink::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* aCallback) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + mRedirectChannel = aNewChannel; + + // Deliberately adding this before confirmation. + nsCOMPtr uri; + aNewChannel->GetOriginalURI(getter_AddRefs(uri)); + if (mPreloader) { + mPreloader->mRedirectRecords.AppendElement( + RedirectRecord(aFlags, uri.forget())); + } + + if (mCallbacks) { + nsCOMPtr sink(do_GetInterface(mCallbacks)); + if (sink) { + return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, + aCallback); + } + } + + aCallback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP PreloaderBase::RedirectSink::OnRedirectResult(nsresult status) { + if (NS_SUCCEEDED(status) && mRedirectChannel) { + mPreloader->mChannel = std::move(mRedirectChannel); + } else { + mRedirectChannel = nullptr; + } + + if (mCallbacks) { + nsCOMPtr sink(do_GetInterface(mCallbacks)); + if (sink) { + return sink->OnRedirectResult(status); + } + } + + return NS_OK; +} + +NS_IMETHODIMP PreloaderBase::RedirectSink::GetInterface(const nsIID& aIID, + void** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink)) || + aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { + return QueryInterface(aIID, aResult); + } + + if (mCallbacks) { + return mCallbacks->GetInterface(aIID, aResult); + } + + *aResult = nullptr; + return NS_ERROR_NO_INTERFACE; +} + +PreloaderBase::~PreloaderBase() { MOZ_ASSERT(NS_IsMainThread()); } + +// static +void PreloaderBase::AddLoadBackgroundFlag(nsIChannel* aChannel) { + nsLoadFlags loadFlags; + aChannel->GetLoadFlags(&loadFlags); + aChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND); +} + +void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey, + dom::Document* aDocument, bool aIsPreload, + bool aIsModule) { + if (aDocument) { + DebugOnly alreadyRegistered = + aDocument->Preloads().RegisterPreload(aKey, this); + // This means there is already a preload registered under this key in this + // document. We only allow replacement when this is a regular load. + // Otherwise, this should never happen and is a suspected misuse of the API. + MOZ_ASSERT_IF(alreadyRegistered, !aIsPreload); + } + + mKey = aKey; + mIsUsed = !aIsPreload; + + // Start usage timer for rel="preload", but not for rel="modulepreload" + // because modules may be loaded for functionality the user does not + // immediately interact with after page load (e.g. a docs search box) + if (!aIsModule && !mIsUsed && !mUsageTimer) { + auto callback = MakeRefPtr(this, aDocument); + NS_NewTimerWithCallback(getter_AddRefs(mUsageTimer), callback, 10000, + nsITimer::TYPE_ONE_SHOT); + } + + ReportUsageTelemetry(); +} + +void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel, + dom::Document* aDocument, bool aIsPreload, + bool aIsModule) { + NotifyOpen(aKey, aDocument, aIsPreload, aIsModule); + mChannel = aChannel; + + nsCOMPtr callbacks; + mChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); + RefPtr sink(new RedirectSink(this, callbacks)); + mChannel->SetNotificationCallbacks(sink); +} + +void PreloaderBase::NotifyUsage(dom::Document* aDocument, + LoadBackground aLoadBackground) { + if (!mIsUsed && mChannel && aLoadBackground == LoadBackground::Drop) { + nsLoadFlags loadFlags; + mChannel->GetLoadFlags(&loadFlags); + + // Preloads are initially set the LOAD_BACKGROUND flag. When becoming + // regular loads by hitting its consuming tag, we need to drop that flag, + // which also means to re-add the request from/to it's loadgroup to reflect + // that flag change. + if (loadFlags & nsIRequest::LOAD_BACKGROUND) { + nsCOMPtr loadGroup; + mChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + + if (loadGroup) { + nsresult status; + mChannel->GetStatus(&status); + + nsresult rv = loadGroup->RemoveRequest(mChannel, nullptr, status); + mChannel->SetLoadFlags(loadFlags & ~nsIRequest::LOAD_BACKGROUND); + if (NS_SUCCEEDED(rv)) { + loadGroup->AddRequest(mChannel, nullptr); + } + } + } + } + + mIsUsed = true; + ReportUsageTelemetry(); + CancelUsageTimer(); + if (mIsEarlyHintsPreload) { + aDocument->Preloads().SetEarlyHintUsed(); + } +} + +void PreloaderBase::RemoveSelf(dom::Document* aDocument) { + if (aDocument) { + aDocument->Preloads().DeregisterPreload(mKey); + } +} + +void PreloaderBase::NotifyRestart(dom::Document* aDocument, + PreloaderBase* aNewPreloader) { + RemoveSelf(aDocument); + mKey = PreloadHashKey(); + + CancelUsageTimer(); + + if (aNewPreloader) { + aNewPreloader->mNodes = std::move(mNodes); + } +} + +void PreloaderBase::NotifyStart(nsIRequest* aRequest) { + // If there is no channel assigned on this preloader, we are not between + // channel switching, so we can freely update the mShouldFireLoadEvent using + // the given channel. + if (mChannel && !SameCOMIdentity(aRequest, mChannel)) { + return; + } + + nsCOMPtr httpChannel = do_QueryInterface(aRequest); + if (!httpChannel) { + return; + } + + // if the load is cross origin without CORS, or the CORS access is rejected, + // always fire load event to avoid leaking site information. + nsresult rv; + nsCOMPtr loadInfo = httpChannel->LoadInfo(); + mShouldFireLoadEvent = + loadInfo->GetTainting() == LoadTainting::Opaque || + (loadInfo->GetTainting() == LoadTainting::CORS && + (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv))); +} + +void PreloaderBase::NotifyStop(nsIRequest* aRequest, nsresult aStatus) { + // Filter out notifications that may be arriving from the old channel before + // restarting this request. + if (!SameCOMIdentity(aRequest, mChannel)) { + return; + } + + NotifyStop(aStatus); +} + +void PreloaderBase::NotifyStop(nsresult aStatus) { + mOnStopStatus.emplace(aStatus); + + nsTArray nodes = std::move(mNodes); + + for (nsWeakPtr& weak : nodes) { + nsCOMPtr node = do_QueryReferent(weak); + if (node) { + NotifyNodeEvent(node); + } + } + + mChannel = nullptr; +} + +void PreloaderBase::AddLinkPreloadNode(nsINode* aNode) { + if (mOnStopStatus) { + return NotifyNodeEvent(aNode); + } + + mNodes.AppendElement(do_GetWeakReference(aNode)); +} + +void PreloaderBase::RemoveLinkPreloadNode(nsINode* aNode) { + // Note that do_GetWeakReference returns the internal weak proxy, which is + // always the same, so we can use it to search the array using default + // comparator. + nsWeakPtr node = do_GetWeakReference(aNode); + mNodes.RemoveElement(node); + + if (kCancelAndRemovePreloadOnZeroReferences && mNodes.Length() == 0 && + !mIsUsed) { + // Keep a reference, because the following call may release us. The caller + // may use a WeakPtr to access this. + RefPtr self(this); + RemoveSelf(aNode->OwnerDoc()); + + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "PreloaderBase::RemoveLinkPreloadNode"_ns); + } + } +} + +void PreloaderBase::NotifyNodeEvent(nsINode* aNode) { + PreloadService::NotifyNodeEvent( + aNode, mShouldFireLoadEvent || NS_SUCCEEDED(*mOnStopStatus)); +} + +void PreloaderBase::CancelUsageTimer() { + if (mUsageTimer) { + mUsageTimer->Cancel(); + mUsageTimer = nullptr; + } +} + +void PreloaderBase::ReportUsageTelemetry() { + if (mUsageTelementryReported) { + return; + } + mUsageTelementryReported = true; + + if (mKey.As() == PreloadHashKey::ResourceType::NONE) { + return; + } + + // The labels are structured as type1-used, type1-unused, type2-used, ... + // The first "as" resource type is NONE with value 0. + auto index = (static_cast(mKey.As()) - 1) * 2; + if (!mIsUsed) { + ++index; + } + + auto label = static_cast(index); + Telemetry::AccumulateCategorical(label); +} + +nsresult PreloaderBase::AsyncConsume(nsIStreamListener* aListener) { + // We want to return an error so that consumers can't ever use a preload to + // consume data unless it's properly implemented. + return NS_ERROR_NOT_IMPLEMENTED; +} + +// PreloaderBase::RedirectRecord + +nsCString PreloaderBase::RedirectRecord::Spec() const { + nsCOMPtr noFragment; + NS_GetURIWithoutRef(mURI, getter_AddRefs(noFragment)); + MOZ_ASSERT(noFragment); + return noFragment->GetSpecOrDefault(); +} + +nsCString PreloaderBase::RedirectRecord::Fragment() const { + nsCString fragment; + mURI->GetRef(fragment); + return fragment; +} + +// PreloaderBase::UsageTimer + +NS_IMPL_ISUPPORTS(PreloaderBase::UsageTimer, nsITimerCallback, nsINamed) + +NS_IMETHODIMP PreloaderBase::UsageTimer::Notify(nsITimer* aTimer) { + if (!mPreload || !mDocument) { + return NS_OK; + } + + MOZ_ASSERT(aTimer == mPreload->mUsageTimer); + mPreload->mUsageTimer = nullptr; + + if (mPreload->IsUsed()) { + // Left in the hashtable, but marked as used. This is a valid case, and we + // don't want to emit a warning for this preload then. + return NS_OK; + } + + mPreload->ReportUsageTelemetry(); + + // PreloadHashKey overrides GetKey, we need to use the nsURIHashKey one to get + // the URI. + nsIURI* uri = static_cast(&mPreload->mKey)->GetKey(); + if (!uri) { + return NS_OK; + } + + nsString spec; + NS_GetSanitizedURIStringFromURI(uri, spec); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + mDocument, nsContentUtils::eDOM_PROPERTIES, + "UnusedLinkPreloadPending", + nsTArray({std::move(spec)})); + return NS_OK; +} + +NS_IMETHODIMP +PreloaderBase::UsageTimer::GetName(nsACString& aName) { + aName.AssignLiteral("PreloaderBase::UsageTimer"); + return NS_OK; +} + +} // namespace mozilla diff --git a/uriloader/preload/PreloaderBase.h b/uriloader/preload/PreloaderBase.h new file mode 100644 index 0000000000..2ecb37a1fd --- /dev/null +++ b/uriloader/preload/PreloaderBase.h @@ -0,0 +1,200 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 PreloaderBase_h__ +#define PreloaderBase_h__ + +#include "mozilla/Maybe.h" +#include "mozilla/PreloadHashKey.h" +#include "mozilla/WeakPtr.h" +#include "nsCOMPtr.h" +#include "nsISupports.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsIWeakReferenceUtils.h" +#include "nsTArray.h" + +class nsIChannel; +class nsINode; +class nsIRequest; +class nsIStreamListener; + +namespace mozilla { + +namespace dom { + +class Document; + +} // namespace dom + +/** + * A half-abstract base class that resource loaders' respective + * channel-listening classes should derive from. Provides a unified API to + * register the load or preload in a document scoped service, links DOM nodes with the load progress and provides API to possibly + * consume the data by later, dynamically discovered consumers. + * + * This class is designed to be used only on the main thread. + */ +class PreloaderBase : public SupportsWeakPtr, public nsISupports { + public: + PreloaderBase() = default; + + // Called by resource loaders to register this preload in the document's + // preload service to provide coalescing, and access to the preload when it + // should be used for an actual load. + void NotifyOpen(const PreloadHashKey& aKey, dom::Document* aDocument, + bool aIsPreload, bool aIsModule = false); + void NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel, + dom::Document* aDocument, bool aIsPreload, + bool aIsModule = false); + + // Called when the load is about to be started all over again and thus this + // PreloaderBase will be registered again with the same key. This method + // taks care to deregister this preload prior to that. + // @param aNewPreloader: If there is a new request and loader created for the + // restarted load, we will pass any necessary information into it. + void NotifyRestart(dom::Document* aDocument, + PreloaderBase* aNewPreloader = nullptr); + + // Called by the loading object when the channel started to load + // (OnStartRequest or equal) and when it finished (OnStopRequest or equal) + void NotifyStart(nsIRequest* aRequest); + void NotifyStop(nsIRequest* aRequest, nsresult aStatus); + // Use this variant only in complement to NotifyOpen without providing a + // channel. + void NotifyStop(nsresult aStatus); + + // Called by resource loaders or any suitable component to notify the preload + // has been used for an actual load. This is intended to stop any usage + // timers. + // @param aDropLoadBackground: If `Keep` then the loading channel, if still in + // progress, will not be removed the LOAD_BACKGROUND flag, for instance XHR is + // the user here. + enum class LoadBackground { Keep, Drop }; + void NotifyUsage(dom::Document* aDocument, + LoadBackground aLoadBackground = LoadBackground::Drop); + // Whether this preloader has been used for a regular/actual load or not. + bool IsUsed() const { return mIsUsed; } + + // Removes itself from the document's preloads hashtable + void RemoveSelf(dom::Document* aDocument); + + // When a loader starting an actual load finds a preload, the data can be + // delivered using this method. It will deliver stream listener notifications + // as if it were coming from the resource loading channel. The |request| + // argument will be the channel that loaded/loads the resource. + // This method must keep to the nsIChannel.AsyncOpen contract. A loader is + // not obligated to re-implement this method when not necessarily needed. + virtual nsresult AsyncConsume(nsIStreamListener* aListener); + + // Accessor to the resource loading channel. + nsIChannel* Channel() const { return mChannel; } + + // May change priority of the resource loading channel so that it's treated as + // preload when this was initially representing a normal speculative load but + // later was found for this resource. + virtual void PrioritizeAsPreload() = 0; + + // Helper function to set the LOAD_BACKGROUND flag on channel initiated by + // . This MUST be used before the channel is AsyncOpen'ed. + static void AddLoadBackgroundFlag(nsIChannel* aChannel); + + // These are linking this preload to DOM nodes. If we + // are already loaded, immediately notify events on the node, otherwise wait + // for NotifyStop() call. + void AddLinkPreloadNode(nsINode* aNode); + void RemoveLinkPreloadNode(nsINode* aNode); + + // A collection of redirects, the main consumer is fetch. + class RedirectRecord { + public: + RedirectRecord(uint32_t aFlags, already_AddRefed aURI) + : mFlags(aFlags), mURI(aURI) {} + + uint32_t Flags() const { return mFlags; } + nsCString Spec() const; + nsCString Fragment() const; + + private: + uint32_t mFlags; + nsCOMPtr mURI; + }; + + const nsTArray& Redirects() { return mRedirectRecords; } + + void SetForEarlyHints() { mIsEarlyHintsPreload = true; } + + protected: + virtual ~PreloaderBase(); + + // The loading channel. This will update when a redirect occurs. + nsCOMPtr mChannel; + + private: + void NotifyNodeEvent(nsINode* aNode); + void CancelUsageTimer(); + + void ReportUsageTelemetry(); + + // A helper class that will update the PreloaderBase.mChannel member when a + // redirect happens, so that we can reprioritize or cancel when needed. + // Having a separate class instead of implementing this on PreloaderBase + // directly is to keep PreloaderBase as simple as possible so that derived + // classes don't have to deal with calling super when implementing these + // interfaces from some reason as well. + class RedirectSink; + + // A timer callback to trigger the unuse warning for this preload + class UsageTimer final : public nsITimerCallback, public nsINamed { + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + UsageTimer(PreloaderBase* aPreload, dom::Document* aDocument); + + private: + ~UsageTimer() = default; + + WeakPtr mDocument; + WeakPtr mPreload; + }; + + private: + // Reference to HTMLLinkElement DOM nodes to deliver onload and onerror + // notifications to. + nsTArray mNodes; + + // History of redirects. + nsTArray mRedirectRecords; + + // Usage timer, emits warning when the preload is not used in time. Started + // in NotifyOpen and stopped in NotifyUsage. + nsCOMPtr mUsageTimer; + + // The key this preload has been registered under. We want to remember it to + // be able to deregister itself from the document's preloads. + PreloadHashKey mKey; + + // This overrides the final event we send to DOM nodes to be always 'load'. + // Modified in NotifyStart based on LoadInfo data of the loading channel. + bool mShouldFireLoadEvent = false; + + // True after call to NotifyUsage. + bool mIsUsed = false; + + // True after we have reported the usage telemetry. Prevent duplicates. + bool mUsageTelementryReported = false; + + // True when this is used to Early Hints preload. + bool mIsEarlyHintsPreload = false; + + // Emplaced when the data delivery has finished, in NotifyStop, holds the + // result of the load. + Maybe mOnStopStatus; +}; + +} // namespace mozilla + +#endif // !PreloaderBase_h__ diff --git a/uriloader/preload/gtest/TestFetchPreloader.cpp b/uriloader/preload/gtest/TestFetchPreloader.cpp new file mode 100644 index 0000000000..67f6eed6f4 --- /dev/null +++ b/uriloader/preload/gtest/TestFetchPreloader.cpp @@ -0,0 +1,950 @@ +#include "gtest/gtest.h" + +#include "mozilla/CORSMode.h" +#include "mozilla/dom/XMLDocument.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "mozilla/FetchPreloader.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/PreloadHashKey.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsIStreamListener.h" +#include "nsThreadUtils.h" +#include "nsStringStream.h" + +namespace { + +auto const ERROR_CANCEL = NS_ERROR_ABORT; +auto const ERROR_ONSTOP = NS_ERROR_NET_INTERRUPT; +auto const ERROR_THROW = NS_ERROR_FAILURE; + +class FakeChannel : public nsIChannel { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUEST + + nsresult Start() { return mListener->OnStartRequest(this); } + nsresult Data(const nsACString& aData) { + if (NS_FAILED(mStatus)) { + return mStatus; + } + nsCOMPtr is; + NS_NewCStringInputStream(getter_AddRefs(is), aData); + return mListener->OnDataAvailable(this, is, 0, aData.Length()); + } + nsresult Stop(nsresult status) { + if (NS_SUCCEEDED(mStatus)) { + mStatus = status; + } + mListener->OnStopRequest(this, mStatus); + mListener = nullptr; + return mStatus; + } + + private: + virtual ~FakeChannel() = default; + bool mIsPending = false; + bool mCanceled = false; + nsresult mStatus = NS_OK; + nsCOMPtr mListener; +}; + +NS_IMPL_ISUPPORTS(FakeChannel, nsIChannel, nsIRequest) + +NS_IMETHODIMP FakeChannel::GetName(nsACString& result) { return NS_OK; } +NS_IMETHODIMP FakeChannel::IsPending(bool* result) { + *result = mIsPending; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetStatus(nsresult* status) { + *status = mStatus; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} +NS_IMETHODIMP FakeChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} +NS_IMETHODIMP FakeChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} +NS_IMETHODIMP FakeChannel::Cancel(nsresult status) { + if (!mCanceled) { + mCanceled = true; + mStatus = status; + } + return NS_OK; +} +NS_IMETHODIMP FakeChannel::Suspend() { return NS_OK; } +NS_IMETHODIMP FakeChannel::Resume() { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = 0; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP FakeChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP FakeChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetOriginalURI(nsIURI** aURI) { return NS_OK; } +NS_IMETHODIMP FakeChannel::SetOriginalURI(nsIURI* aURI) { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetURI(nsIURI** aURI) { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetOwner(nsISupports** aOwner) { return NS_OK; } +NS_IMETHODIMP FakeChannel::SetOwner(nsISupports* aOwner) { return NS_OK; } +NS_IMETHODIMP FakeChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetIsDocument(bool* aIsDocument) { + *aIsDocument = false; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aCallbacks) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentType(nsACString& aContentType) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentType(const nsACString& aContentType) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentCharset(nsACString& aContentCharset) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentCharset( + const nsACString& aContentCharset) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentDisposition( + uint32_t* aContentDisposition) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentDisposition(uint32_t aContentDisposition) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} +NS_IMETHODIMP FakeChannel::GetContentLength(int64_t* aContentLength) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentLength(int64_t aContentLength) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetCanceled(bool* aCanceled) { + *aCanceled = mCanceled; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::Open(nsIInputStream** aStream) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP +FakeChannel::AsyncOpen(nsIStreamListener* aListener) { + mIsPending = true; + mListener = aListener; + return NS_OK; +} + +class FakePreloader : public mozilla::FetchPreloader { + public: + explicit FakePreloader(FakeChannel* aChannel) : mDrivingChannel(aChannel) {} + + private: + RefPtr mDrivingChannel; + + virtual nsresult CreateChannel( + nsIChannel** aChannel, nsIURI* aURI, const mozilla::CORSMode aCORSMode, + const mozilla::dom::ReferrerPolicy& aReferrerPolicy, + mozilla::dom::Document* aDocument, nsILoadGroup* aLoadGroup, + nsIInterfaceRequestor* aCallbacks, uint64_t aHttpChannelId) override { + mDrivingChannel.forget(aChannel); + return NS_OK; + } +}; + +class FakeListener : public nsIStreamListener { + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + enum { Never, OnStart, OnData, OnStop } mCancelIn = Never; + + nsresult mOnStartResult = NS_OK; + nsresult mOnDataResult = NS_OK; + nsresult mOnStopResult = NS_OK; + + bool mOnStart = false; + nsCString mOnData; + mozilla::Maybe mOnStop; + + private: + virtual ~FakeListener() = default; +}; + +NS_IMPL_ISUPPORTS(FakeListener, nsIStreamListener, nsIRequestObserver) + +NS_IMETHODIMP FakeListener::OnStartRequest(nsIRequest* request) { + EXPECT_FALSE(mOnStart); + mOnStart = true; + + if (mCancelIn == OnStart) { + request->Cancel(ERROR_CANCEL); + } + + return mOnStartResult; +} +NS_IMETHODIMP FakeListener::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, uint32_t count) { + nsAutoCString data; + data.SetLength(count); + + uint32_t read; + input->Read(data.BeginWriting(), count, &read); + mOnData += data; + + if (mCancelIn == OnData) { + request->Cancel(ERROR_CANCEL); + } + + return mOnDataResult; +} +NS_IMETHODIMP FakeListener::OnStopRequest(nsIRequest* request, + nsresult status) { + EXPECT_FALSE(mOnStop); + mOnStop.emplace(status); + + if (mCancelIn == OnStop) { + request->Cancel(ERROR_CANCEL); + } + + return mOnStopResult; +} + +bool eventInProgress = true; + +void Await() { + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "uriloader:TestFetchPreloader:Await"_ns, [&]() { + bool yield = !eventInProgress; + eventInProgress = true; // Just for convenience + return yield; + })); +} + +void Yield() { eventInProgress = false; } + +} // namespace + +// **************************************************************************** +// Test body +// **************************************************************************** + +// Caching with all good results (data + NS_OK) +TEST(TestFetchPreloader, CacheNoneBeforeConsume) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + RefPtr listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheStartBeforeConsume) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + + RefPtr listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartOfDataBeforeConsume) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + + RefPtr listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllDataBeforeConsume) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + + // Request consumation of the preload... + RefPtr listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsume) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Get data before the channel fails +TEST(TestFetchPreloader, CacheAllBeforeConsumeWithChannelError) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_FAILED(channel->Stop(ERROR_ONSTOP)); + + RefPtr listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_ONSTOP); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Cancel the channel between caching and consuming +TEST(TestFetchPreloader, CacheAllBeforeConsumeWithChannelCancel) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + channel->Cancel(ERROR_CANCEL); + EXPECT_NS_FAILED(channel->Stop(ERROR_CANCEL)); + + RefPtr listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + // XXX - This is hard to solve; the data is there but we won't deliver it. + // This is a bit different case than e.g. a network error. We want to + // deliver some data in that case. Cancellation probably happens because of + // navigation or a demand to not consume the channel anyway. + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Let the listener throw while data is already cached +TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnStartRequest) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr listener = new FakeListener(); + listener->mOnStartResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnDataAvailable) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr listener = new FakeListener(); + listener->mOnDataResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnStopRequest) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr listener = new FakeListener(); + listener->mOnStopResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + // Throwing from OnStopRequest is generally ignored. + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Cancel the channel in various callbacks +TEST(TestFetchPreloader, CacheAllBeforeConsumeCancelInOnStartRequest) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnStart; + // check that throwing from OnStartRequest doesn't affect the cancellation + // status. + listener->mOnStartResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsumeCancelInOnDataAvailable) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnData; + // check that throwing from OnStartRequest doesn't affect the cancellation + // status. + listener->mOnDataResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Corner cases +TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnDataAvailable) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + + RefPtr listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnData; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_NS_FAILED(channel->Data("three"_ns)); + EXPECT_NS_FAILED(channel->Stop(NS_OK)); + + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnStartRequestAndRace) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + + // This has to simulate a possibiilty when stream listener notifications from + // the channel are already pending in the queue while AsyncConsume is called. + // At this moment the listener has not been notified yet. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + })); + + RefPtr listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnStart; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + // Check listener's been fed properly. Expected is to NOT get any data and + // propagate the cancellation code and not being called duplicated + // OnStopRequest. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnDataAvailableAndRace) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + + // This has to simulate a possibiilty when stream listener notifications from + // the channel are already pending in the queue while AsyncConsume is called. + // At this moment the listener has not been notified yet. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + })); + + RefPtr listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnData; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + // Check listener's been fed properly. Expected is to NOT get anything after + // the first OnData and propagate the cancellation code and not being called + // duplicated OnStopRequest. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartlyBeforeConsumeThrowFromOnStartRequestAndRace) +{ + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr channel = new FakeChannel(); + RefPtr preloader = new FakePreloader(channel); + RefPtr doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc, 0))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + + // This has to simulate a possibiilty when stream listener notifications from + // the channel are already pending in the queue while AsyncConsume is called. + // At this moment the listener has not been notified yet. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + })); + + RefPtr listener = new FakeListener(); + listener->mOnStartResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + // Check listener's been fed properly. Expected is to NOT get any data and + // propagate the throwing code and not being called duplicated OnStopRequest. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} diff --git a/uriloader/preload/gtest/moz.build b/uriloader/preload/gtest/moz.build new file mode 100644 index 0000000000..72c2afa539 --- /dev/null +++ b/uriloader/preload/gtest/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; c-basic-offset: 4; 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/. + +UNIFIED_SOURCES += [ + "TestFetchPreloader.cpp", +] + +LOCAL_INCLUDES += [ + "/netwerk/base", + "/xpcom/tests/gtest", +] + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += ["!/xpcom", "/xpcom/components"] diff --git a/uriloader/preload/moz.build b/uriloader/preload/moz.build new file mode 100644 index 0000000000..27a2f8cf00 --- /dev/null +++ b/uriloader/preload/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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking") + +TEST_DIRS += ["gtest"] + +EXPORTS.mozilla += [ + "FetchPreloader.h", + "PreloaderBase.h", + "PreloadHashKey.h", + "PreloadService.h", +] + +UNIFIED_SOURCES += [ + "FetchPreloader.cpp", + "PreloaderBase.cpp", + "PreloadHashKey.cpp", + "PreloadService.cpp", +] + +FINAL_LIBRARY = "xul" -- cgit v1.2.3