Bug 1858546 - part1 : ensure LPAC permission on Widevine L1 path. r=bobowen,aosmond
Differential Revision: https://phabricator.services.mozilla.com/D192888
This commit is contained in:
@@ -29,6 +29,8 @@ inline constexpr char kWidevineExperimentKeySystemName[] =
|
|||||||
// hardware decryption with codecs that support clear lead.
|
// hardware decryption with codecs that support clear lead.
|
||||||
inline constexpr char kWidevineExperiment2KeySystemName[] =
|
inline constexpr char kWidevineExperiment2KeySystemName[] =
|
||||||
"com.widevine.alpha.experiment2";
|
"com.widevine.alpha.experiment2";
|
||||||
|
// API name used for searching GMP Wideivine L1 plugin.
|
||||||
|
inline constexpr char kWidevineExperimentAPIName[] = "windows-mf-cdm";
|
||||||
|
|
||||||
// https://learn.microsoft.com/en-us/playready/overview/key-system-strings
|
// https://learn.microsoft.com/en-us/playready/overview/key-system-strings
|
||||||
inline constexpr char kPlayReadyKeySystemName[] =
|
inline constexpr char kPlayReadyKeySystemName[] =
|
||||||
|
|||||||
@@ -1106,8 +1106,8 @@ RefPtr<GenericPromise> GMPParent::ParseChromiumManifest(
|
|||||||
mAdapter = u"chromium"_ns;
|
mAdapter = u"chromium"_ns;
|
||||||
#ifdef MOZ_WMF_CDM
|
#ifdef MOZ_WMF_CDM
|
||||||
} else if (mPluginType == GMPPluginType::WidevineL1) {
|
} else if (mPluginType == GMPPluginType::WidevineL1) {
|
||||||
video.mAPIName = "windows-mf-cdm"_ns;
|
video.mAPIName = nsCString(kWidevineExperimentAPIName);
|
||||||
mAdapter = u"windows-mf-cdm"_ns;
|
mAdapter = NS_ConvertUTF8toUTF16(kWidevineExperimentAPIName);
|
||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
GMP_PARENT_LOG_DEBUG("%s: CDM API not supported, failing.", __FUNCTION__);
|
GMP_PARENT_LOG_DEBUG("%s: CDM API not supported, failing.", __FUNCTION__);
|
||||||
@@ -1232,7 +1232,7 @@ void GMPParent::UpdatePluginType() {
|
|||||||
if (mDisplayName.EqualsLiteral("WidevineCdm")) {
|
if (mDisplayName.EqualsLiteral("WidevineCdm")) {
|
||||||
mPluginType = GMPPluginType::Widevine;
|
mPluginType = GMPPluginType::Widevine;
|
||||||
#ifdef MOZ_WMF_CDM
|
#ifdef MOZ_WMF_CDM
|
||||||
} else if (mDisplayName.EqualsLiteral("windows-mf-cdm")) {
|
} else if (mDisplayName.EqualsLiteral(kWidevineExperimentAPIName)) {
|
||||||
mPluginType = GMPPluginType::WidevineL1;
|
mPluginType = GMPPluginType::WidevineL1;
|
||||||
#endif
|
#endif
|
||||||
} else if (mDisplayName.EqualsLiteral("gmpopenh264")) {
|
} else if (mDisplayName.EqualsLiteral("gmpopenh264")) {
|
||||||
|
|||||||
@@ -350,6 +350,13 @@ GeckoMediaPluginServiceChild::HasPluginForAPI(const nsACString& aAPI,
|
|||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NS_IMETHODIMP
|
||||||
|
GeckoMediaPluginServiceChild::FindPluginDirectoryForAPI(
|
||||||
|
const nsACString& aAPI, const nsTArray<nsCString>& aTags,
|
||||||
|
nsIFile** aDirectory) {
|
||||||
|
return NS_ERROR_NOT_IMPLEMENTED;
|
||||||
|
}
|
||||||
|
|
||||||
NS_IMETHODIMP
|
NS_IMETHODIMP
|
||||||
GeckoMediaPluginServiceChild::GetNodeId(
|
GeckoMediaPluginServiceChild::GetNodeId(
|
||||||
const nsAString& aOrigin, const nsAString& aTopLevelOrigin,
|
const nsAString& aOrigin, const nsAString& aTopLevelOrigin,
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class GeckoMediaPluginServiceChild : public GeckoMediaPluginService,
|
|||||||
NS_IMETHOD HasPluginForAPI(const nsACString& aAPI,
|
NS_IMETHOD HasPluginForAPI(const nsACString& aAPI,
|
||||||
const nsTArray<nsCString>& aTags,
|
const nsTArray<nsCString>& aTags,
|
||||||
bool* aRetVal) override;
|
bool* aRetVal) override;
|
||||||
|
NS_IMETHOD FindPluginDirectoryForAPI(const nsACString& aAPI,
|
||||||
|
const nsTArray<nsCString>& aTags,
|
||||||
|
nsIFile** aDirectory) override;
|
||||||
NS_IMETHOD GetNodeId(const nsAString& aOrigin,
|
NS_IMETHOD GetNodeId(const nsAString& aOrigin,
|
||||||
const nsAString& aTopLevelOrigin,
|
const nsAString& aTopLevelOrigin,
|
||||||
const nsAString& aGMPName,
|
const nsAString& aGMPName,
|
||||||
|
|||||||
@@ -904,6 +904,33 @@ GeckoMediaPluginServiceParent::HasPluginForAPI(const nsACString& aAPI,
|
|||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NS_IMETHODIMP
|
||||||
|
GeckoMediaPluginServiceParent::FindPluginDirectoryForAPI(
|
||||||
|
const nsACString& aAPI, const nsTArray<nsCString>& aTags,
|
||||||
|
nsIFile** aDirectory) {
|
||||||
|
NS_ENSURE_ARG(!aTags.IsEmpty());
|
||||||
|
NS_ENSURE_ARG(aDirectory);
|
||||||
|
|
||||||
|
nsresult rv = EnsurePluginsOnDiskScanned();
|
||||||
|
if (NS_FAILED(rv)) {
|
||||||
|
NS_WARNING("Failed to load GMPs from disk.");
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
MutexAutoLock lock(mMutex);
|
||||||
|
nsCString api(aAPI);
|
||||||
|
size_t index = 0;
|
||||||
|
RefPtr<GMPParent> gmp = FindPluginForAPIFrom(index, api, aTags, &index);
|
||||||
|
if (gmp) {
|
||||||
|
nsCOMPtr<nsIFile> dir = gmp->GetDirectory();
|
||||||
|
dir.forget(aDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
nsresult GeckoMediaPluginServiceParent::EnsurePluginsOnDiskScanned() {
|
nsresult GeckoMediaPluginServiceParent::EnsurePluginsOnDiskScanned() {
|
||||||
const char* env = nullptr;
|
const char* env = nullptr;
|
||||||
if (!mScannedPluginOnDisk && (env = PR_GetEnv("MOZ_GMP_PATH")) && *env) {
|
if (!mScannedPluginOnDisk && (env = PR_GetEnv("MOZ_GMP_PATH")) && *env) {
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ class GeckoMediaPluginServiceParent final
|
|||||||
NS_IMETHOD HasPluginForAPI(const nsACString& aAPI,
|
NS_IMETHOD HasPluginForAPI(const nsACString& aAPI,
|
||||||
const nsTArray<nsCString>& aTags,
|
const nsTArray<nsCString>& aTags,
|
||||||
bool* aRetVal) override;
|
bool* aRetVal) override;
|
||||||
|
NS_IMETHOD FindPluginDirectoryForAPI(const nsACString& aAPI,
|
||||||
|
const nsTArray<nsCString>& aTags,
|
||||||
|
nsIFile** aDirectory) override;
|
||||||
NS_IMETHOD GetNodeId(const nsAString& aOrigin,
|
NS_IMETHOD GetNodeId(const nsAString& aOrigin,
|
||||||
const nsAString& aTopLevelOrigin,
|
const nsAString& aTopLevelOrigin,
|
||||||
const nsAString& aGMPName,
|
const nsAString& aGMPName,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include "nsISupports.idl"
|
#include "nsISupports.idl"
|
||||||
#include "nsIThread.idl"
|
#include "nsIThread.idl"
|
||||||
|
|
||||||
|
interface nsIFile;
|
||||||
|
|
||||||
%{C++
|
%{C++
|
||||||
#include "mozilla/UniquePtr.h"
|
#include "mozilla/UniquePtr.h"
|
||||||
#include "nsTArray.h"
|
#include "nsTArray.h"
|
||||||
@@ -77,6 +79,13 @@ interface mozIGeckoMediaPluginService : nsISupports
|
|||||||
[noscript]
|
[noscript]
|
||||||
boolean hasPluginForAPI(in ACString api, in ConstTagArrayRef tags);
|
boolean hasPluginForAPI(in ACString api, in ConstTagArrayRef tags);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the plugin directory for a plugin that supports the specified tags.
|
||||||
|
* Callable on any thread
|
||||||
|
*/
|
||||||
|
[noscript]
|
||||||
|
nsIFile findPluginDirectoryForAPI(in ACString api, in ConstTagArrayRef tags);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a video decoder that supports the specified tags.
|
* Get a video decoder that supports the specified tags.
|
||||||
* The array of tags should at least contain a codec tag, and optionally
|
* The array of tags should at least contain a codec tag, and optionally
|
||||||
|
|||||||
@@ -28,6 +28,15 @@
|
|||||||
# include "mozilla/WinDllServices.h"
|
# include "mozilla/WinDllServices.h"
|
||||||
#endif // defined(XP_WIN)
|
#endif // defined(XP_WIN)
|
||||||
|
|
||||||
|
#if defined(MOZ_WMF_CDM) && defined(MOZ_SANDBOX)
|
||||||
|
# include "GMPServiceParent.h"
|
||||||
|
# include "mozilla/dom/KeySystemNames.h"
|
||||||
|
# include "mozilla/MFMediaEngineUtils.h"
|
||||||
|
# include "mozilla/StaticPrefs_media.h"
|
||||||
|
# include "nsIFile.h"
|
||||||
|
# include "sandboxBroker.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "ProfilerParent.h"
|
#include "ProfilerParent.h"
|
||||||
#include "mozilla/PProfilerChild.h"
|
#include "mozilla/PProfilerChild.h"
|
||||||
|
|
||||||
@@ -36,6 +45,12 @@ namespace mozilla::ipc {
|
|||||||
LazyLogModule gUtilityProcessLog("utilityproc");
|
LazyLogModule gUtilityProcessLog("utilityproc");
|
||||||
#define LOGD(...) MOZ_LOG(gUtilityProcessLog, LogLevel::Debug, (__VA_ARGS__))
|
#define LOGD(...) MOZ_LOG(gUtilityProcessLog, LogLevel::Debug, (__VA_ARGS__))
|
||||||
|
|
||||||
|
#if defined(MOZ_WMF_CDM) && defined(MOZ_SANDBOX)
|
||||||
|
# define WMF_LOG(msg, ...) \
|
||||||
|
MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
|
||||||
|
("UtilityProcessHost=%p, " msg, this, ##__VA_ARGS__))
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
|
#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
|
||||||
bool UtilityProcessHost::sLaunchWithMacSandbox = false;
|
bool UtilityProcessHost::sLaunchWithMacSandbox = false;
|
||||||
#endif
|
#endif
|
||||||
@@ -91,6 +106,10 @@ bool UtilityProcessHost::Launch(StringVector aExtraOpts) {
|
|||||||
mSandboxLevel = Preferences::GetInt("security.sandbox.utility.level");
|
mSandboxLevel = Preferences::GetInt("security.sandbox.utility.level");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(MOZ_WMF_CDM) && defined(MOZ_SANDBOX)
|
||||||
|
EnsureWidevineL1PathForSandbox();
|
||||||
|
#endif
|
||||||
|
|
||||||
mLaunchPhase = LaunchPhase::Waiting;
|
mLaunchPhase = LaunchPhase::Waiting;
|
||||||
|
|
||||||
if (!GeckoChildProcessHost::AsyncLaunch(aExtraOpts)) {
|
if (!GeckoChildProcessHost::AsyncLaunch(aExtraOpts)) {
|
||||||
@@ -336,4 +355,54 @@ MacSandboxType UtilityProcessHost::GetMacSandboxType() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(MOZ_WMF_CDM) && defined(MOZ_SANDBOX)
|
||||||
|
void UtilityProcessHost::EnsureWidevineL1PathForSandbox() {
|
||||||
|
if (mSandbox != SandboxingKind::MF_MEDIA_ENGINE_CDM) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefPtr<mozilla::gmp::GeckoMediaPluginServiceParent> gmps =
|
||||||
|
mozilla::gmp::GeckoMediaPluginServiceParent::GetSingleton();
|
||||||
|
if (NS_WARN_IF(!gmps)) {
|
||||||
|
WMF_LOG("Failed to get GeckoMediaPluginServiceParent!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StaticPrefs::media_eme_widevine_experiment_enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static nsString sWidevineL1Path;
|
||||||
|
if (sWidevineL1Path.IsEmpty()) {
|
||||||
|
// TODO : install L1 if it's not downloaded yet in bug 1863800.
|
||||||
|
nsCOMPtr<nsIFile> pluginFile;
|
||||||
|
if (NS_WARN_IF(NS_FAILED(gmps->FindPluginDirectoryForAPI(
|
||||||
|
nsCString(kWidevineExperimentAPIName),
|
||||||
|
{nsCString(kWidevineExperimentKeySystemName)},
|
||||||
|
getter_AddRefs(pluginFile))))) {
|
||||||
|
WMF_LOG("Widevine L1 is not installed yet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pluginFile) {
|
||||||
|
WMF_LOG("No plugin file found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NS_WARN_IF(NS_FAILED(pluginFile->GetTarget(sWidevineL1Path)))) {
|
||||||
|
WMF_LOG("Failed to get L1 path!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOZ_ASSERT(!sWidevineL1Path.IsEmpty());
|
||||||
|
WMF_LOG("Store Widevine L1 path=%s",
|
||||||
|
NS_ConvertUTF16toUTF8(sWidevineL1Path).get());
|
||||||
|
}
|
||||||
|
SandboxBroker::EnsureLpacPermsissionsOnDir(sWidevineL1Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
# undef WMF_LOG
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
} // namespace mozilla::ipc
|
} // namespace mozilla::ipc
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ class UtilityProcessHost final : public mozilla::ipc::GeckoChildProcessHost {
|
|||||||
void RejectPromise();
|
void RejectPromise();
|
||||||
void ResolvePromise();
|
void ResolvePromise();
|
||||||
|
|
||||||
|
#if defined(MOZ_WMF_CDM) && defined(MOZ_SANDBOX)
|
||||||
|
void EnsureWidevineL1PathForSandbox();
|
||||||
|
#endif
|
||||||
|
|
||||||
// Set to true on construction and to false just prior deletion.
|
// Set to true on construction and to false just prior deletion.
|
||||||
// The UtilityProcessHost isn't refcounted; so we can capture this by value in
|
// The UtilityProcessHost isn't refcounted; so we can capture this by value in
|
||||||
// lambdas along with a strong reference to mLiveToken and check if that value
|
// lambdas along with a strong reference to mLiveToken and check if that value
|
||||||
|
|||||||
@@ -616,7 +616,8 @@ static void HexEncode(const Span<const uint8_t>& aBytes, nsACString& aEncoded) {
|
|||||||
// This is left as a void because we might fail to set the permission for some
|
// This is left as a void because we might fail to set the permission for some
|
||||||
// reason and yet the LPAC permission is already granted. So returning success
|
// reason and yet the LPAC permission is already granted. So returning success
|
||||||
// or failure isn't really that useful.
|
// or failure isn't really that useful.
|
||||||
static void EnsureLpacPermsissionsOnBinDir() {
|
/* static */
|
||||||
|
void SandboxBroker::EnsureLpacPermsissionsOnDir(const nsString& aDir) {
|
||||||
// For MSIX packages we get access through the packageContents capability and
|
// For MSIX packages we get access through the packageContents capability and
|
||||||
// we probably won't have access to add the permission either way.
|
// we probably won't have access to add the permission either way.
|
||||||
if (widget::WinUtils::HasPackageIdentity()) {
|
if (widget::WinUtils::HasPackageIdentity()) {
|
||||||
@@ -632,28 +633,28 @@ static void EnsureLpacPermsissionsOnBinDir() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HANDLE hBinDir =
|
HANDLE hDir = ::CreateFileW(aDir.get(), WRITE_DAC | READ_CONTROL, 0, NULL,
|
||||||
::CreateFileW(sBinDir->get(), WRITE_DAC | READ_CONTROL, 0, NULL,
|
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||||
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
if (hDir == INVALID_HANDLE_VALUE) {
|
||||||
if (hBinDir == INVALID_HANDLE_VALUE) {
|
LOG_W("Unable to get directory handle for %s",
|
||||||
LOG_W("Unable to get binary directory handle.");
|
NS_ConvertUTF16toUTF8(aDir).get());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UniquePtr<HANDLE, CloseHandleDeleter> autoHandleCloser(hBinDir);
|
UniquePtr<HANDLE, CloseHandleDeleter> autoHandleCloser(hDir);
|
||||||
PACL pBinDirAcl = nullptr;
|
PACL pBinDirAcl = nullptr;
|
||||||
PSECURITY_DESCRIPTOR pSD = nullptr;
|
PSECURITY_DESCRIPTOR pSD = nullptr;
|
||||||
DWORD result =
|
DWORD result =
|
||||||
::GetSecurityInfo(hBinDir, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
|
::GetSecurityInfo(hDir, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
|
||||||
nullptr, nullptr, &pBinDirAcl, nullptr, &pSD);
|
nullptr, nullptr, &pBinDirAcl, nullptr, &pSD);
|
||||||
if (result != ERROR_SUCCESS) {
|
if (result != ERROR_SUCCESS) {
|
||||||
LOG_E("Failed to get DACL for binary directory.");
|
LOG_E("Failed to get DACL for %s", NS_ConvertUTF16toUTF8(aDir).get());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UniquePtr<VOID, LocalFreeDeleter> autoFreeSecDesc(pSD);
|
UniquePtr<VOID, LocalFreeDeleter> autoFreeSecDesc(pSD);
|
||||||
if (!pBinDirAcl) {
|
if (!pBinDirAcl) {
|
||||||
LOG_E("DACL for binary directory was null.");
|
LOG_E("DACL was null for %s", NS_ConvertUTF16toUTF8(aDir).get());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +673,8 @@ static void EnsureLpacPermsissionsOnBinDir() {
|
|||||||
|
|
||||||
PSID aceSID = reinterpret_cast<PSID>(&(pAllowedAce->SidStart));
|
PSID aceSID = reinterpret_cast<PSID>(&(pAllowedAce->SidStart));
|
||||||
if (::EqualSid(aceSID, lpacFirefoxInstallFilesSid)) {
|
if (::EqualSid(aceSID, lpacFirefoxInstallFilesSid)) {
|
||||||
LOG_D("Firefox install files permission found on binary directory.");
|
LOG_D("Firefox install files permission found on %s",
|
||||||
|
NS_ConvertUTF16toUTF8(aDir).get());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -690,13 +692,14 @@ static void EnsureLpacPermsissionsOnBinDir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UniquePtr<ACL, LocalFreeDeleter> autoFreeAcl(newDacl);
|
UniquePtr<ACL, LocalFreeDeleter> autoFreeAcl(newDacl);
|
||||||
if (ERROR_SUCCESS != ::SetSecurityInfo(hBinDir, SE_FILE_OBJECT,
|
if (ERROR_SUCCESS != ::SetSecurityInfo(hDir, SE_FILE_OBJECT,
|
||||||
DACL_SECURITY_INFORMATION, nullptr,
|
DACL_SECURITY_INFORMATION, nullptr,
|
||||||
nullptr, newDacl, nullptr)) {
|
nullptr, newDacl, nullptr)) {
|
||||||
LOG_E("Failed to set new DACL on binary directory.");
|
LOG_E("Failed to set new DACL on %s", NS_ConvertUTF16toUTF8(aDir).get());
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_D("Firefox install files permission granted on binary directory.");
|
LOG_D("Firefox install files permission granted on %s",
|
||||||
|
NS_ConvertUTF16toUTF8(aDir).get());
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsLowPrivilegedAppContainerSupported() {
|
static bool IsLowPrivilegedAppContainerSupported() {
|
||||||
@@ -731,7 +734,7 @@ static sandbox::ResultCode AddAndConfigureAppContainerProfile(
|
|||||||
::LoadLibraryW(L"userenv.dll");
|
::LoadLibraryW(L"userenv.dll");
|
||||||
|
|
||||||
// Done during the package string initialization so we only do it once.
|
// Done during the package string initialization so we only do it once.
|
||||||
EnsureLpacPermsissionsOnBinDir();
|
SandboxBroker::EnsureLpacPermsissionsOnDir(*sBinDir.get());
|
||||||
|
|
||||||
// This mirrors Edge's use of the exe path for the SHA1 hash to give a
|
// This mirrors Edge's use of the exe path for the SHA1 hash to give a
|
||||||
// machine unique name per install.
|
// machine unique name per install.
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ class SandboxBroker : public AbstractSandboxBroker {
|
|||||||
|
|
||||||
static void Initialize(sandbox::BrokerServices* aBrokerServices);
|
static void Initialize(sandbox::BrokerServices* aBrokerServices);
|
||||||
|
|
||||||
|
static void EnsureLpacPermsissionsOnDir(const nsString& aDir);
|
||||||
|
|
||||||
void Shutdown() override {}
|
void Shutdown() override {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user