Bug 1602191: Create a shortcut on the windows desktop to launch an installed ssb. r=mhowell,Gijs

Adds an XPCOM API for creating a windows shortcut and uses it to create one
when installing a SSB.

Differential Revision: https://phabricator.services.mozilla.com/D56292

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Dave Townsend 2019-12-16 20:11:04 +00:00
parent 82b556f808
commit 541fc82db0
10 changed files with 218 additions and 5 deletions

View File

@ -35,6 +35,9 @@ elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gtk':
'nsGNOMEShellService.cpp',
]
elif CONFIG['OS_ARCH'] == 'WINNT':
XPIDL_SOURCES += [
'nsIWindowsShellService.idl',
]
SOURCES += [
'nsWindowsShellService.cpp',
]

View File

@ -0,0 +1,13 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
interface nsIFile;
[scriptable, uuid(fb9b59db-5a91-4e67-92b6-35e7d6e6d3fd)]
interface nsIWindowsShellService : nsISupports
{
void createShortcut(in nsIFile aBinary, in Array<AString> aArguments, in AString aDescription, in nsIFile aTarget);
};

View File

@ -57,11 +57,29 @@
#define APP_REG_NAME_BASE L"Firefox-"
#ifdef DEBUG
# define NS_ENSURE_HRESULT(hres, ret) \
do { \
HRESULT result = hres; \
if (MOZ_UNLIKELY(FAILED(result))) { \
mozilla::SmprintfPointer msg = mozilla::Smprintf( \
"NS_ENSURE_HRESULT(%s, %s) failed with " \
"result 0x%" PRIX32, \
#hres, #ret, static_cast<uint32_t>(result)); \
NS_WARNING(msg.get()); \
return ret; \
} \
} while (false)
#else
# define NS_ENSURE_HRESULT(hres, ret) \
if (MOZ_UNLIKELY(FAILED(hres))) return ret
#endif
using mozilla::IsWin8OrLater;
using namespace mozilla;
NS_IMPL_ISUPPORTS(nsWindowsShellService, nsIToolkitShellService,
nsIShellService)
nsIShellService, nsIWindowsShellService)
static nsresult OpenKeyForReading(HKEY aKeyRoot, const nsAString& aKeyName,
HKEY* aKey) {
@ -679,6 +697,45 @@ nsWindowsShellService::SetDesktopBackgroundColor(uint32_t aColor) {
return regKey->Close();
}
NS_IMETHODIMP
nsWindowsShellService::CreateShortcut(nsIFile* aBinary,
const nsTArray<nsString>& aArguments,
const nsAString& aDescription,
nsIFile* aTarget) {
NS_ENSURE_ARG(aBinary);
NS_ENSURE_ARG(aTarget);
RefPtr<IShellLinkW> link;
HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
IID_IShellLinkW, getter_AddRefs(link));
NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE);
nsString path(aBinary->NativePath());
link->SetPath(path.get());
if (!aDescription.IsEmpty()) {
link->SetDescription(PromiseFlatString(aDescription).get());
}
// TODO: Properly escape quotes in the string, see bug 1604287.
nsString arguments;
for (auto& arg : aArguments) {
arguments.AppendPrintf("\"%S\" ", arg.get());
}
link->SetArguments(arguments.get());
RefPtr<IPersistFile> persist;
hr = link->QueryInterface(IID_IPersistFile, getter_AddRefs(persist));
NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE);
nsString target(aTarget->NativePath());
hr = persist->Save(target.get(), TRUE);
NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE);
return NS_OK;
}
nsWindowsShellService::nsWindowsShellService() {}
nsWindowsShellService::~nsWindowsShellService() {}

View File

@ -10,12 +10,14 @@
#include "nsString.h"
#include "nsToolkitShellService.h"
#include "nsIShellService.h"
#include "nsIWindowsShellService.h"
#include <windows.h>
#include <ole2.h>
class nsWindowsShellService : public nsIShellService,
public nsToolkitShellService {
public nsToolkitShellService,
public nsIWindowsShellService {
virtual ~nsWindowsShellService();
public:
@ -23,6 +25,7 @@ class nsWindowsShellService : public nsIShellService,
NS_DECL_ISUPPORTS
NS_DECL_NSISHELLSERVICE
NS_DECL_NSIWINDOWSSHELLSERVICE
protected:
nsresult LaunchControlPanelDefaultsSelectionUI();

View File

@ -39,8 +39,17 @@ XPCOMUtils.defineLazyModuleGetters(this, {
KeyValueService: "resource://gre/modules/kvstore.jsm",
OS: "resource://gre/modules/osfile.jsm",
ImageTools: "resource:///modules/ssb/ImageTools.jsm",
AppConstants: "resource://gre/modules/AppConstants.jsm",
});
if (AppConstants.platform == "win") {
ChromeUtils.defineModuleGetter(
this,
"WindowsSupport",
"resource:///modules/ssb/WindowsSupport.jsm"
);
}
/**
* A schema version for the SSB data stored in the kvstore.
*
@ -418,9 +427,13 @@ class SiteSpecificBrowser extends SiteSpecificBrowserBase {
);
}
return SiteSpecificBrowser.createFromManifest(
await buildManifestForBrowser(browser)
);
let manifest = await buildManifestForBrowser(browser);
let ssb = SiteSpecificBrowser.createFromManifest(manifest);
if (!manifest.name) {
ssb.name = browser.contentTitle;
}
return ssb;
}
/**
@ -479,6 +492,10 @@ class SiteSpecificBrowser extends SiteSpecificBrowserBase {
this._config.persisted = true;
await this._maybeSave();
if (AppConstants.platform == "win") {
await WindowsSupport.install(this);
}
}
/**
@ -490,6 +507,10 @@ class SiteSpecificBrowser extends SiteSpecificBrowserBase {
return;
}
if (AppConstants.platform == "win") {
await WindowsSupport.uninstall(this);
}
this._config.persisted = false;
let kvstore = await SiteSpecificBrowserService.getKVStore();
await kvstore.delete(storeKey(this.id));
@ -502,6 +523,23 @@ class SiteSpecificBrowser extends SiteSpecificBrowserBase {
return this._id;
}
get name() {
if (this._config.name) {
return this._config.name;
}
if (this._manifest.name) {
return this._manifest.name;
}
return this.startURI.host;
}
set name(val) {
this._config.name = val;
this._maybeSave();
}
/**
* The default URI to load.
*/
@ -658,6 +696,19 @@ const SiteSpecificBrowserService = {
return this.kvstore;
},
/**
* Checks if OS integration is enabled. This will affect whether installs and
* uninstalls have effects on the OS itself amongst other things. Generally
* only disabled for testing.
*/
get useOSIntegration() {
if (Services.appinfo.OS != "WINNT") {
return false;
}
return Services.prefs.getBoolPref("browser.ssb.osintegration", true);
},
/**
* Returns a promise that resolves to an array of all of the installed SSBs.
*/

View File

@ -0,0 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var EXPORTED_SYMBOLS = ["WindowsSupport"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { SiteSpecificBrowserService } = ChromeUtils.import(
"resource:///modules/SiteSpecificBrowserService.jsm"
);
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
const shellService = Cc["@mozilla.org/browser/shell-service;1"].getService(
Ci.nsIWindowsShellService
);
const File = Components.Constructor(
"@mozilla.org/file/local;1",
Ci.nsIFile,
"initWithPath"
);
const WindowsSupport = {
/**
* Installs an SSB by creating a shortcut to launch it on the user's desktop.
*
* @param {SiteSpecificBrowser} ssb the SSB to install.
*/
async install(ssb) {
if (!SiteSpecificBrowserService.useOSIntegration) {
return;
}
let desktop = Services.dirsvc.get("Desk", Ci.nsIFile);
let link = OS.Path.join(desktop.path, `${ssb.name}.lnk`);
shellService.createShortcut(
Services.dirsvc.get("XREExeF", Ci.nsIFile),
["-profile", OS.Constants.Path.profileDir, "-start-ssb", ssb.id],
ssb.name,
new File(link)
);
},
/**
* Uninstalls an SSB by deleting its shortcut from the user's desktop.
*
* @param {SiteSpecificBrowser} ssb the SSB to uninstall.
*/
async uninstall(ssb) {
if (!SiteSpecificBrowserService.useOSIntegration) {
return;
}
let desktop = Services.dirsvc.get("Desk", Ci.nsIFile);
let link = OS.Path.join(desktop.path, `${ssb.name}.lnk`);
try {
await OS.File.remove(link);
} catch (e) {
console.error(e);
}
},
};

View File

@ -24,3 +24,8 @@ FINAL_TARGET_FILES.actors += [
'SiteSpecificBrowserChild.jsm',
'SiteSpecificBrowserParent.jsm',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
EXTRA_JS_MODULES.ssb += [
'WindowsSupport.jsm',
]

View File

@ -5,6 +5,7 @@ support-files =
empty_page.html
prefs =
browser.ssb.enabled=true
browser.ssb.osintegration=false
[browser_ssb_direct.js]
[browser_ssb_lasttab.js]

View File

@ -13,6 +13,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
ManifestProcessor: "resource://gre/modules/ManifestProcessor.jsm",
KeyValueService: "resource://gre/modules/kvstore.jsm",
OS: "resource://gre/modules/osfile.jsm",
AppConstants: "resource://gre/modules/AppConstants.jsm",
});
const SSB_STORE_PREFIX = "ssb:";
@ -25,6 +26,7 @@ let gSSBData = gProfD.clone();
gSSBData.append("ssb");
Services.prefs.setBoolPref("browser.ssb.enabled", true);
Services.prefs.setBoolPref("browser.ssb.osintegration", false);
async function getKVStore() {
await OS.File.makeDir(gSSBData.path);

View File

@ -3,6 +3,8 @@
// Tests that installing adds it to the store.
add_task(async () => {
Services.prefs.setBoolPref("browser.ssb.osintegration", true);
let ssb = SiteSpecificBrowser.createFromURI(uri("https://www.mozilla.org/"));
await ssb.install();
@ -17,4 +19,15 @@ add_task(async () => {
let data = JSON.parse(await kvstore.get(`ssb:${ssb.id}`));
Assert.ok("manifest" in data);
Assert.ok("config" in data);
if (AppConstants.platform == "win") {
// Check that the shortcut is made and destroyed.
let link = Services.dirsvc.get("Desk", Ci.nsIFile);
link.append("www.mozilla.org.lnk");
Assert.ok(link.isFile());
await ssb.uninstall();
Assert.ok(!link.exists());
}
});