Merge remote-tracking branch 'origin/master' into media-works

This commit is contained in:
2021-09-27 22:29:13 +08:00
74 changed files with 3199 additions and 1724 deletions
+1 -1
View File
@@ -347,7 +347,7 @@ getCharacterExtentsCB(AtkText *aText, gint aOffset,
if (aCoords == ATK_XY_SCREEN) {
geckoCoordType = nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE;
} else {
giabbaCoordType = nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE;
geckoCoordType = nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE;
}
AccessibleWrap* accWrap = GetAccessibleWrap(ATK_OBJECT(aText));
+82 -93
View File
@@ -204,7 +204,88 @@ NotificationController::WillRefresh(mozilla::TimeStamp aTime)
}
// Process rendered text change notifications.
mTextHash.EnumerateEntries(TextEnumerator, mDocument);
for (auto iter = mTextHash.Iter(); !iter.Done(); iter.Next()) {
nsCOMPtrHashKey<nsIContent>* entry = iter.Get();
nsIContent* textNode = entry->GetKey();
Accessible* textAcc = mDocument->GetAccessible(textNode);
// If the text node is not in tree or doesn't have frame then this case should
// have been handled already by content removal notifications.
nsINode* containerNode = textNode->GetParentNode();
if (!containerNode) {
NS_ASSERTION(!textAcc,
"Text node was removed but accessible is kept alive!");
continue;
}
nsIFrame* textFrame = textNode->GetPrimaryFrame();
if (!textFrame) {
NS_ASSERTION(!textAcc,
"Text node isn't rendered but accessible is kept alive!");
continue;
}
nsIContent* containerElm = containerNode->IsElement() ?
containerNode->AsElement() : nullptr;
nsIFrame::RenderedText text = textFrame->GetRenderedText();
// Remove text accessible if rendered text is empty.
if (textAcc) {
if (text.mString.IsEmpty()) {
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eTree | logging::eText)) {
logging::MsgBegin("TREE", "text node lost its content");
logging::Node("container", containerElm);
logging::Node("content", textNode);
logging::MsgEnd();
}
#endif
mDocument->ContentRemoved(containerElm, textNode);
continue;
}
// Update text of the accessible and fire text change events.
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eText)) {
logging::MsgBegin("TEXT", "text may be changed");
logging::Node("container", containerElm);
logging::Node("content", textNode);
logging::MsgEntry("old text '%s'",
NS_ConvertUTF16toUTF8(textAcc->AsTextLeaf()->Text()).get());
logging::MsgEntry("new text: '%s'",
NS_ConvertUTF16toUTF8(text.mString).get());
logging::MsgEnd();
}
#endif
TextUpdater::Run(mDocument, textAcc->AsTextLeaf(), text.mString);
continue;
}
// Append an accessible if rendered text is not empty.
if (!text.mString.IsEmpty()) {
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eTree | logging::eText)) {
logging::MsgBegin("TREE", "text node gains new content");
logging::Node("container", containerElm);
logging::Node("content", textNode);
logging::MsgEnd();
}
#endif
// Make sure the text node is in accessible document still.
Accessible* container = mDocument->GetAccessibleOrContainer(containerNode);
NS_ASSERTION(container,
"Text node having rendered text hasn't accessible document!");
if (container) {
nsTArray<nsCOMPtr<nsIContent> > insertedContents;
insertedContents.AppendElement(textNode);
mDocument->ProcessContentInserted(container, &insertedContents);
}
}
}
mTextHash.Clear();
// Bind hanging child documents.
@@ -314,98 +395,6 @@ NotificationController::WillRefresh(mozilla::TimeStamp aTime)
}
}
////////////////////////////////////////////////////////////////////////////////
// Notification controller: text leaf accessible text update
PLDHashOperator
NotificationController::TextEnumerator(nsCOMPtrHashKey<nsIContent>* aEntry,
void* aUserArg)
{
DocAccessible* document = static_cast<DocAccessible*>(aUserArg);
nsIContent* textNode = aEntry->GetKey();
Accessible* textAcc = document->GetAccessible(textNode);
// If the text node is not in tree or doesn't have frame then this case should
// have been handled already by content removal notifications.
nsINode* containerNode = textNode->GetParentNode();
if (!containerNode) {
NS_ASSERTION(!textAcc,
"Text node was removed but accessible is kept alive!");
return PL_DHASH_NEXT;
}
nsIFrame* textFrame = textNode->GetPrimaryFrame();
if (!textFrame) {
NS_ASSERTION(!textAcc,
"Text node isn't rendered but accessible is kept alive!");
return PL_DHASH_NEXT;
}
nsIContent* containerElm = containerNode->IsElement() ?
containerNode->AsElement() : nullptr;
nsIFrame::RenderedText text = textFrame->GetRenderedText();
// Remove text accessible if rendered text is empty.
if (textAcc) {
if (text.mString.IsEmpty()) {
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eTree | logging::eText)) {
logging::MsgBegin("TREE", "text node lost its content");
logging::Node("container", containerElm);
logging::Node("content", textNode);
logging::MsgEnd();
}
#endif
document->ContentRemoved(containerElm, textNode);
return PL_DHASH_NEXT;
}
// Update text of the accessible and fire text change events.
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eText)) {
logging::MsgBegin("TEXT", "text may be changed");
logging::Node("container", containerElm);
logging::Node("content", textNode);
logging::MsgEntry("old text '%s'",
NS_ConvertUTF16toUTF8(textAcc->AsTextLeaf()->Text()).get());
logging::MsgEntry("new text: '%s'",
NS_ConvertUTF16toUTF8(text.mString).get());
logging::MsgEnd();
}
#endif
TextUpdater::Run(document, textAcc->AsTextLeaf(), text.mString);
return PL_DHASH_NEXT;
}
// Append an accessible if rendered text is not empty.
if (!text.mString.IsEmpty()) {
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eTree | logging::eText)) {
logging::MsgBegin("TREE", "text node gains new content");
logging::Node("container", containerElm);
logging::Node("content", textNode);
logging::MsgEnd();
}
#endif
// Make sure the text node is in accessible document still.
Accessible* container = document->GetAccessibleOrContainer(containerNode);
NS_ASSERTION(container,
"Text node having rendered text hasn't accessible document!");
if (container) {
nsTArray<nsCOMPtr<nsIContent> > insertedContents;
insertedContents.AppendElement(textNode);
document->ProcessContentInserted(container, &insertedContents);
}
}
return PL_DHASH_NEXT;
}
////////////////////////////////////////////////////////////////////////////////
// NotificationController: content inserted notification
-6
View File
@@ -297,12 +297,6 @@ private:
*/
nsTHashtable<nsCOMPtrHashKey<nsIContent> > mTextHash;
/**
* Update the accessible tree for pending rendered text change notifications.
*/
static PLDHashOperator TextEnumerator(nsCOMPtrHashKey<nsIContent>* aEntry,
void* aUserArg);
/**
* Other notifications like DOM events. Don't make this an nsAutoTArray; we
* use SwapElements() on it.
+1 -1
View File
@@ -458,7 +458,7 @@ DocAccessible::Shutdown()
// XXX thinking about ordering?
if (IPCAccessibilityActive()) {
DocAccessibleChild::Send__delete__(mIPCDoc);
mIPCDoc->Shutdown();
MOZ_ASSERT(!mIPCDoc);
}
+6
View File
@@ -52,6 +52,12 @@ SerializeTree(Accessible* aRoot, nsTArray<AccessibleData>& aTree)
Accessible*
DocAccessibleChild::IdToAccessible(const uint64_t& aID) const
{
if (!aID)
return mDoc;
if (!mDoc)
return nullptr;
return mDoc->GetAccessibleByUniqueID(reinterpret_cast<void*>(aID));
}
+12 -1
View File
@@ -32,10 +32,21 @@ public:
{ MOZ_COUNT_CTOR(DocAccessibleChild); }
~DocAccessibleChild()
{
mDoc->SetIPCDoc(nullptr);
// Shutdown() should have been called, but maybe it isn't if the process is
// killed?
MOZ_ASSERT(!mDoc);
if (mDoc)
mDoc->SetIPCDoc(nullptr);
MOZ_COUNT_DTOR(DocAccessibleChild);
}
void Shutdown()
{
mDoc->SetIPCDoc(nullptr);
mDoc = nullptr;
SendShutdown();
}
void ShowEvent(AccShowEvent* aShowEvent);
/*
+34 -8
View File
@@ -8,6 +8,7 @@
#include "nsAutoPtr.h"
#include "mozilla/a11y/Platform.h"
#include "ProxyAccessible.h"
#include "mozilla/dom/TabParent.h"
namespace mozilla {
namespace a11y {
@@ -101,6 +102,8 @@ DocAccessibleParent::RecvHideEvent(const uint64_t& aRootID)
if (mShutdown)
return true;
CheckDocTree();
ProxyEntry* rootEntry = mAccessibles.GetEntry(aRootID);
if (!rootEntry) {
NS_ERROR("invalid root being removed!");
@@ -167,11 +170,16 @@ DocAccessibleParent::AddChildDoc(DocAccessibleParent* aChildDoc,
return true;
}
PLDHashOperator
DocAccessibleParent::ShutdownAccessibles(ProxyEntry* entry, void*)
bool
DocAccessibleParent::RecvShutdown()
{
ProxyDestroyed(entry->mProxy);
return PL_DHASH_REMOVE;
Destroy();
if (!static_cast<dom::TabParent*>(Manager())->IsDestroyed()) {
return PDocAccessibleParent::Send__delete__(this);
}
return true;
}
void
@@ -186,10 +194,28 @@ DocAccessibleParent::Destroy()
for (uint32_t i = childDocCount - 1; i < childDocCount; i--)
mChildDocs[i]->Destroy();
mAccessibles.EnumerateEntries(ShutdownAccessibles, nullptr);
for (auto iter = mAccessibles.Iter(); !iter.Done(); iter.Next()) {
ProxyDestroyed(iter.Get()->mProxy);
iter.Remove();
}
ProxyDestroyed(this);
mParentDoc ? mParentDoc->RemoveChildDoc(this)
: GetAccService()->RemoteDocShutdown(this);
}
if (mParentDoc)
mParentDoc->RemoveChildDoc(this);
else if (IsTopLevel())
GetAccService()->RemoteDocShutdown(this);
}
void
DocAccessibleParent::CheckDocTree() const
{
size_t childDocs = mChildDocs.Length();
for (size_t i = 0; i < childDocs; i++) {
if (!mChildDocs[i] || mChildDocs[i]->mParentDoc != this)
MOZ_CRASH("document tree is broken!");
mChildDocs[i]->CheckDocTree();
}
}
} // a11y
} // mozilla
+12 -5
View File
@@ -26,15 +26,19 @@ class DocAccessibleParent : public ProxyAccessible,
{
public:
DocAccessibleParent() :
ProxyAccessible(this), mParentDoc(nullptr), mShutdown(false)
ProxyAccessible(this), mParentDoc(nullptr),
mTopLevel(false), mShutdown(false)
{ MOZ_COUNT_CTOR_INHERITED(DocAccessibleParent, ProxyAccessible); }
~DocAccessibleParent()
{
MOZ_COUNT_DTOR_INHERITED(DocAccessibleParent, ProxyAccessible);
MOZ_ASSERT(mChildDocs.Length() == 0);
MOZ_ASSERT(!mParentDoc);
MOZ_ASSERT(!ParentDoc());
}
void SetTopLevel() { mTopLevel = true; }
bool IsTopLevel() const { return mTopLevel; }
/*
* Called when a message from a document in a child process notifies the main
* process it is firing an event.
@@ -49,10 +53,11 @@ public:
void Unbind()
{
mParent = nullptr;
mParentDoc->mChildDocs.RemoveElement(this);
ParentDoc()->mChildDocs.RemoveElement(this);
mParentDoc = nullptr;
}
virtual bool RecvShutdown() override;
void Destroy();
virtual void ActorDestroy(ActorDestroyReason aWhy) override
{
@@ -64,7 +69,7 @@ public:
* Return the main processes representation of the parent document (if any)
* of the document this object represents.
*/
DocAccessibleParent* Parent() const { return mParentDoc; }
DocAccessibleParent* ParentDoc() const { return mParentDoc; }
/*
* Called when a document in a content process notifies the main process of a
@@ -128,7 +133,8 @@ private:
uint32_t AddSubtree(ProxyAccessible* aParent,
const nsTArray<AccessibleData>& aNewTree, uint32_t aIdx,
uint32_t aIdxInParent);
static PLDHashOperator ShutdownAccessibles(ProxyEntry* entry, void* unused);
void CheckDocTree() const;
nsTArray<DocAccessibleParent*> mChildDocs;
DocAccessibleParent* mParentDoc;
@@ -138,6 +144,7 @@ private:
* proxy object so we can't use a real map.
*/
nsTHashtable<ProxyEntry> mAccessibles;
bool mTopLevel;
bool mShutdown;
};
+3 -1
View File
@@ -48,7 +48,7 @@ prio(normal upto high) sync protocol PDocAccessible
manager PBrowser;
parent:
__delete__();
Shutdown();
/*
* Notify the parent process the document in the child process is firing an
@@ -65,6 +65,8 @@ parent:
BindChildDoc(PDocAccessible aChildDoc, uint64_t aID);
child:
__delete__();
// Accessible
prio(high) sync State(uint64_t aID) returns(uint64_t states);
prio(high) sync Name(uint64_t aID) returns(nsString name);
+3
View File
@@ -262,6 +262,9 @@ pref("browser.urlbar.clickSelectsAll", true);
pref("browser.urlbar.doubleClickSelectsAll", false);
pref("browser.urlbar.autoFill", true);
pref("browser.urlbar.autoFill.typed", true);
pref("browser.urlbar.unifiedcomplete", false);
// 0: Match anywhere (e.g., middle of words)
// 1: Match on word boundaries and then try matching anywhere
// 2: Match only on word boundaries (e.g., after / or .)
+44 -26
View File
@@ -2578,33 +2578,51 @@ ContentParent::RecvSetClipboard(const IPCDataTransfer& aDataTransfer,
NS_ENSURE_SUCCESS(rv, true);
} else if (item.data().type() == IPCDataTransferData::TnsCString) {
const IPCDataTransferImage& imageDetails = item.imageDetails();
const gfxIntSize size(imageDetails.width(), imageDetails.height());
if (!size.width || !size.height) {
return true;
if (item.flavor().EqualsLiteral(kJPEGImageMime) ||
item.flavor().EqualsLiteral(kJPGImageMime) ||
item.flavor().EqualsLiteral(kPNGImageMime) ||
item.flavor().EqualsLiteral(kGIFImageMime)) {
const IPCDataTransferImage& imageDetails = item.imageDetails();
const gfxIntSize size(imageDetails.width(), imageDetails.height());
if (!size.width || !size.height) {
return true;
}
nsCString text = item.data().get_nsCString();
mozilla::RefPtr<gfx::DataSourceSurface> image =
new mozilla::gfx::SourceSurfaceRawData();
mozilla::gfx::SourceSurfaceRawData* raw =
static_cast<mozilla::gfx::SourceSurfaceRawData*>(image.get());
raw->InitWrappingData(
reinterpret_cast<uint8_t*>(const_cast<nsCString&>(text).BeginWriting()),
size, imageDetails.stride(),
static_cast<mozilla::gfx::SurfaceFormat>(imageDetails.format()), false);
raw->GuaranteePersistance();
nsRefPtr<gfxDrawable> drawable = new gfxSurfaceDrawable(image, size);
nsCOMPtr<imgIContainer> imageContainer(image::ImageOps::CreateFromDrawable(drawable));
nsCOMPtr<nsISupportsInterfacePointer>
imgPtr(do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv));
rv = imgPtr->SetData(imageContainer);
NS_ENSURE_SUCCESS(rv, true);
trans->SetTransferData(item.flavor().get(), imgPtr, sizeof(nsISupports*));
} else {
nsCOMPtr<nsISupportsCString> dataWrapper =
do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, true);
const nsCString& text = item.data().get_nsCString();
rv = dataWrapper->SetData(text);
NS_ENSURE_SUCCESS(rv, true);
rv = trans->SetTransferData(item.flavor().get(), dataWrapper,
text.Length());
NS_ENSURE_SUCCESS(rv, true);
}
nsCString text = item.data().get_nsCString();
mozilla::RefPtr<gfx::DataSourceSurface> image =
new mozilla::gfx::SourceSurfaceRawData();
mozilla::gfx::SourceSurfaceRawData* raw =
static_cast<mozilla::gfx::SourceSurfaceRawData*>(image.get());
raw->InitWrappingData(
reinterpret_cast<uint8_t*>(const_cast<nsCString&>(text).BeginWriting()),
size, imageDetails.stride(),
static_cast<mozilla::gfx::SurfaceFormat>(imageDetails.format()), false);
raw->GuaranteePersistance();
nsRefPtr<gfxDrawable> drawable = new gfxSurfaceDrawable(image, size);
nsCOMPtr<imgIContainer> imageContainer(image::ImageOps::CreateFromDrawable(drawable));
nsCOMPtr<nsISupportsInterfacePointer>
imgPtr(do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv));
rv = imgPtr->SetData(imageContainer);
NS_ENSURE_SUCCESS(rv, true);
trans->SetTransferData(item.flavor().get(), imgPtr, sizeof(nsISupports*));
}
}
+1
View File
@@ -1183,6 +1183,7 @@ TabParent::RecvPDocAccessibleConstructor(PDocAccessibleParent* aDoc,
return parentDoc->AddChildDoc(doc, aParentID);
} else {
MOZ_ASSERT(!aParentID);
doc->SetTopLevel();
a11y::DocManager::RemoteDocAdded(doc);
}
#endif
+1 -1
View File
@@ -504,7 +504,7 @@ private:
void DoInjection(const nsAutoHandle& aSnapshot);
static DWORD WINAPI GetToolhelpSnapshot(LPVOID aContext);
void OnCrash(DWORD processID) MOZ_OVERRIDE;
void OnCrash(DWORD processID) override;
DWORD mFlashProcess1;
DWORD mFlashProcess2;
+25 -3
View File
@@ -1000,11 +1000,13 @@ nsPermissionManager::AddInternal(nsIPrincipal* aPrincipal,
}
NS_IMETHODIMP
nsPermissionManager::Remove(const nsACString &aHost,
const char *aType)
nsPermissionManager::Remove(nsIURI* aURI,
const char* aType)
{
NS_ENSURE_ARG_POINTER(aURI);
nsCOMPtr<nsIPrincipal> principal;
nsresult rv = GetPrincipal(aHost, getter_AddRefs(principal));
nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal));
NS_ENSURE_SUCCESS(rv, rv);
return RemoveFromPrincipal(principal, aType);
@@ -1040,6 +1042,26 @@ nsPermissionManager::RemoveFromPrincipal(nsIPrincipal* aPrincipal,
eWriteToDB);
}
NS_IMETHODIMP
nsPermissionManager::RemovePermission(nsIPermission* aPerm)
{
nsAutoCString host;
nsresult rv = aPerm->GetHost(host);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIPrincipal> principal;
rv = GetPrincipal(host, getter_AddRefs(principal));
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString type;
rv = aPerm->GetType(type);
NS_ENSURE_SUCCESS(rv, rv);
// Permissions are uniquely identified by their principal and type.
// We remove the permission using these two pieces of data.
return RemoveFromPrincipal(principal, type.get());
}
NS_IMETHODIMP
nsPermissionManager::RemoveAll()
{
@@ -0,0 +1,67 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function run_test() {
// initialize the permission manager service
let pm = Cc["@mozilla.org/permissionmanager;1"].
getService(Ci.nsIPermissionManager);
do_check_eq(perm_count(), 0);
// add some permissions
let uri = NetUtil.newURI("http://amazon.com:8080/foobarbaz", null, null);
let uri2 = NetUtil.newURI("http://google.com:2048/quxx", null, null);
pm.add(uri, "apple", 0);
pm.add(uri, "apple", 3);
pm.add(uri, "pear", 3);
pm.add(uri, "pear", 1);
pm.add(uri, "cucumber", 1);
pm.add(uri, "cucumber", 1);
pm.add(uri, "cucumber", 1);
pm.add(uri2, "apple", 2);
pm.add(uri2, "pear", 0);
pm.add(uri2, "pear", 2);
// Make sure that removePermission doesn't remove more than one permission each time
do_check_eq(perm_count(), 5);
remove_one_by_type("apple");
do_check_eq(perm_count(), 4);
remove_one_by_type("apple");
do_check_eq(perm_count(), 3);
remove_one_by_type("pear");
do_check_eq(perm_count(), 2);
remove_one_by_type("cucumber");
do_check_eq(perm_count(), 1);
remove_one_by_type("pear");
do_check_eq(perm_count(), 0);
function perm_count() {
let enumerator = pm.enumerator;
let count = 0;
while (enumerator.hasMoreElements()) {
count++;
enumerator.getNext();
}
return count;
}
function remove_one_by_type(type) {
let enumerator = pm.enumerator;
while (enumerator.hasMoreElements()) {
let it = enumerator.getNext().QueryInterface(Ci.nsIPermission);
if (it.type == type) {
pm.removePermission(it);
break;
}
}
}
}
+1
View File
@@ -34,3 +34,4 @@ skip-if = debug == true
[test_permmanager_cleardata.js]
[test_schema_2_migration.js]
[test_schema_3_migration.js]
[test_permmanager_removepermission.js]
+30 -4
View File
@@ -429,11 +429,11 @@ DrawTargetSkia::DrawSurfaceWithShadow(SourceSurface *aSurface,
SkPaint paint;
SkImageFilter* filter = SkDropShadowImageFilter::Create(aOffset.x, aOffset.y,
aSigma, aSigma,
ColorToSkColor(aColor, 1.0));
SkAutoTUnref<SkImageFilter> filter(SkDropShadowImageFilter::Create(aOffset.x, aOffset.y,
aSigma, aSigma,
ColorToSkColor(aColor, 1.0)));
paint.setImageFilter(filter);
paint.setImageFilter(filter.get());
paint.setXfermodeMode(GfxOpToSkiaOp(aOperator));
mCanvas->drawBitmap(bitmap.mBitmap, aDest.x, aDest.y, &paint);
@@ -525,6 +525,26 @@ DrawTargetSkia::Fill(const Path *aPath,
mCanvas->drawPath(skiaPath->GetPath(), paint.mPaint);
}
bool
DrawTargetSkia::ShouldLCDRenderText(FontType aFontType, AntialiasMode aAntialiasMode)
{
// For non-opaque surfaces, only allow subpixel AA if explicitly permitted.
if (!IsOpaque(mFormat) && !mPermitSubpixelAA) {
return false;
}
if (aAntialiasMode == AntialiasMode::DEFAULT) {
switch (aFontType) {
case FontType::MAC:
return true;
default:
// TODO: Figure out what to do for the other platforms.
return false;
}
}
return (aAntialiasMode == AntialiasMode::SUBPIXEL);
}
void
DrawTargetSkia::FillGlyphs(ScaledFont *aFont,
const GlyphBuffer &aBuffer,
@@ -547,6 +567,9 @@ DrawTargetSkia::FillGlyphs(ScaledFont *aFont,
paint.mPaint.setTextSize(SkFloatToScalar(skiaFont->mSize));
paint.mPaint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
bool shouldLCDRenderText = ShouldLCDRenderText(aFont->GetType(), aOptions.mAntialiasMode);
paint.mPaint.setLCDRenderText(shouldLCDRenderText);
if (aRenderingOptions && aRenderingOptions->GetType() == FontType::CAIRO) {
switch (static_cast<const GlyphRenderingOptionsCairo*>(aRenderingOptions)->GetHinting()) {
case FontHinting::NONE:
@@ -566,6 +589,9 @@ DrawTargetSkia::FillGlyphs(ScaledFont *aFont,
if (static_cast<const GlyphRenderingOptionsCairo*>(aRenderingOptions)->GetAutoHinting()) {
paint.mPaint.setAutohinted(true);
}
} else if (aFont->GetType() == FontType::MAC && shouldLCDRenderText) {
// SkFontHost_mac only supports subpixel antialiasing when hinting is turned off.
paint.mPaint.setHinting(SkPaint::kNo_Hinting);
} else {
paint.mPaint.setHinting(SkPaint::kNormal_Hinting);
}
+2
View File
@@ -129,6 +129,8 @@ private:
void MarkChanged();
bool ShouldLCDRenderText(FontType aFontType, AntialiasMode aAntialiasMode);
SkRect SkRectCoveringWholeSurface() const;
bool UsingSkiaGPU() const;
+1 -1
View File
@@ -105,7 +105,7 @@ SourceSurfaceSkia::InitFromTexture(DrawTargetSkia* aOwner,
mSize.width = skiaTexGlue.fWidth = aSize.width;
mSize.height = skiaTexGlue.fHeight = aSize.height;
skiaTexGlue.fFlags = kNone_GrBackendTextureFlag;
skiaTexGlue.fOrigin = kBottomLeft_GrSurfaceOrigin;
skiaTexGlue.fOrigin = kTopLeft_GrSurfaceOrigin;
skiaTexGlue.fConfig = GfxFormatToGrConfig(aFormat);
skiaTexGlue.fSampleCnt = 0;
skiaTexGlue.fTextureHandle = aTexture;
+2
View File
@@ -23,6 +23,8 @@ enum class CreateContextFlags : int8_t {
REQUIRE_COMPAT_PROFILE = 1 << 0,
// Force the use of hardware backed GL, don't allow software implementations.
FORCE_ENABLE_HARDWARE = 1 << 1,
/* Don't force discrete GPU to be used (if applicable) */
ALLOW_OFFLINE_RENDERER = 1 << 2,
};
MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(CreateContextFlags)
+17 -4
View File
@@ -187,6 +187,11 @@ static const NSOpenGLPixelFormatAttribute kAttribs_offscreen[] = {
0
};
static const NSOpenGLPixelFormatAttribute kAttribs_offscreen_allow_offline[] = {
NSOpenGLPFAAllowOfflineRenderers,
0
};
static const NSOpenGLPixelFormatAttribute kAttribs_offscreen_accel[] = {
NSOpenGLPFAAccelerated,
0
@@ -271,10 +276,18 @@ CreateOffscreenFBOContext(CreateContextFlags flags)
if (!context) {
profile = ContextProfile::OpenGLCompatibility;
if (gfxPrefs::RequireHardwareGL())
context = CreateWithFormat(kAttribs_offscreen_accel);
else
context = CreateWithFormat(kAttribs_offscreen);
if (flags & CreateContextFlags::ALLOW_OFFLINE_RENDERER) {
if (gfxPrefs::RequireHardwareGL())
context = CreateWithFormat(kAttribs_singleBuffered);
else
context = CreateWithFormat(kAttribs_offscreen_allow_offline);
} else {
if (gfxPrefs::RequireHardwareGL())
context = CreateWithFormat(kAttribs_offscreen_accel);
else
context = CreateWithFormat(kAttribs_offscreen);
}
}
if (!context) {
NS_WARNING("Failed to create NSOpenGLContext.");
+2 -1
View File
@@ -1147,7 +1147,8 @@ gfxPlatform::GetSkiaGLGlue()
* stands, this only works on the main thread.
*/
nsRefPtr<GLContext> glContext;
glContext = GLContextProvider::CreateHeadless(CreateContextFlags::REQUIRE_COMPAT_PROFILE);
glContext = GLContextProvider::CreateHeadless(CreateContextFlags::REQUIRE_COMPAT_PROFILE |
CreateContextFlags::ALLOW_OFFLINE_RENDERER);
if (!glContext) {
printf_stderr("Failed to create GLContext for SkiaGL!\n");
return nullptr;
+2 -1
View File
@@ -653,7 +653,8 @@ RasterImage::CopyFrame(uint32_t aWhichFrame, uint32_t aFlags)
}
DataSourceSurface::MappedSurface mapping;
if (NS_WARN_IF(!surf->Map(DataSourceSurface::MapType::WRITE, &mapping))) {
if (!surf->Map(DataSourceSurface::MapType::WRITE, &mapping)) {
gfxCriticalError() << "RasterImage::CopyFrame failed to map surface";
return nullptr;
}
+1 -1
View File
@@ -9,7 +9,7 @@
#include "base/process.h"
#include "mozilla/Mutex.h"
#if defined(OS_LINUX) || defined(OS_MACOSX)
#if defined(OS_LINUX) || defined(XP_DARWIN)
#include <pthread.h>
#include "SharedMemoryBasic.h"
#include "mozilla/Atomics.h"
+1
View File
@@ -0,0 +1 @@
%/SharedMemoryBasic_mach.cpp: ;
+1 -1
View File
@@ -10,7 +10,7 @@
#ifdef ANDROID
# include "mozilla/ipc/SharedMemoryBasic_android.h"
#elif defined(XP_MACOSX)
#elif defined(XP_DARWIN)
# include "mozilla/ipc/SharedMemoryBasic_mach.h"
#else
# include "mozilla/ipc/SharedMemoryBasic_chromium.h"
@@ -9,7 +9,20 @@
#include <mach/vm_map.h>
#include <mach/mach_port.h>
#if defined(XP_IOS)
#include <mach/vm_map.h>
#define mach_vm_address_t vm_address_t
#define mach_vm_allocate vm_allocate
#define mach_vm_deallocate vm_deallocate
#define mach_vm_map vm_map
#define mach_vm_read vm_read
#define mach_vm_region_recurse vm_region_recurse_64
#define mach_vm_size_t vm_size_t
#else
#include <mach/mach_vm.h>
#endif
#include <pthread.h>
#include <unistd.h>
#include "SharedMemoryBasic.h"
#include "chrome/common/mach_ipc_mac.h"
@@ -94,6 +107,7 @@ enum {
};
const int kTimeout = 1000;
const int kLongTimeout = 60 * kTimeout;
pid_t gParentPid = 0;
@@ -114,6 +128,11 @@ struct ListeningThread {
: mThread(thread), mPorts(ports) {}
};
struct SharePortsReply {
uint64_t serial;
mach_port_t port;
};
std::map<pid_t, ListeningThread> gThreads;
static void *
@@ -264,8 +283,13 @@ void
HandleSharePortsMessage(MachReceiveMessage* rmsg, MemoryPorts* ports)
{
mach_port_t port = rmsg->GetTranslatedPort(0);
uint64_t* serial = reinterpret_cast<uint64_t*>(rmsg->GetData());
MachSendMessage msg(kReturnIdMsg);
msg.SetData(&port, sizeof(port));
// Construct the reply message, echoing the serial, and adding the port
SharePortsReply replydata;
replydata.port = port;
replydata.serial = *serial;
msg.SetData(&replydata, sizeof(SharePortsReply));
kern_return_t err = ports->mSender->SendMessage(msg, kTimeout);
if (KERN_SUCCESS != err) {
LOG_ERROR("SendMessage failed 0x%x %s\n", err, mach_error_string(err));
@@ -565,8 +589,18 @@ bool
SharedMemoryBasic::ShareToProcess(base::ProcessId pid,
Handle* aNewHandle)
{
if (pid == getpid()) {
*aNewHandle = mPort;
return mach_port_mod_refs(mach_task_self(), *aNewHandle, MACH_PORT_RIGHT_SEND, 1) == KERN_SUCCESS;
}
StaticMutexAutoLock smal(gMutex);
// Serially number the messages, to check whether
// the reply we get was meant for us.
static uint64_t serial = 0;
uint64_t my_serial = serial;
serial++;
MemoryPorts* ports = GetMemoryPortsForPid(pid);
if (!ports) {
LOG_ERROR("Unable to get ports for process.\n");
@@ -574,6 +608,7 @@ SharedMemoryBasic::ShareToProcess(base::ProcessId pid,
}
MachSendMessage smsg(kSharePortsMsg);
smsg.AddDescriptor(MachMsgPortDescriptor(mPort, MACH_MSG_TYPE_COPY_SEND));
smsg.SetData(&my_serial, sizeof(uint64_t));
kern_return_t err = ports->mSender->SendMessage(smsg, kTimeout);
if (err != KERN_SUCCESS) {
LOG_ERROR("sending port failed %s %x\n", mach_error_string(err), err);
@@ -582,15 +617,27 @@ SharedMemoryBasic::ShareToProcess(base::ProcessId pid,
MachReceiveMessage msg;
err = ports->mReceiver->WaitForMessage(&msg, kTimeout);
if (err != KERN_SUCCESS) {
LOG_ERROR("didn't get an id %s %x\n", mach_error_string(err), err);
return false;
LOG_ERROR("short timeout didn't get an id %s %x\n", mach_error_string(err), err);
MOZ_ASSERT(false, "Receiver message short time out");
err = ports->mReceiver->WaitForMessage(&msg, kLongTimeout);
if (err != KERN_SUCCESS) {
LOG_ERROR("long timeout didn't get an id %s %x\n", mach_error_string(err), err);
return false;
}
}
if (msg.GetDataLength() != sizeof(mach_port_t)) {
if (msg.GetDataLength() != sizeof(SharePortsReply)) {
LOG_ERROR("Improperly formatted reply\n");
return false;
}
mach_port_t *id = reinterpret_cast<mach_port_t*>(msg.GetData());
*aNewHandle = *id;
SharePortsReply* msg_data = reinterpret_cast<SharePortsReply*>(msg.GetData());
mach_port_t id = msg_data->port;
uint64_t serial_check = msg_data->serial;
if (serial_check != my_serial) {
LOG_ERROR("Serials do not match up: %d vs %d", serial_check, my_serial);
return false;
}
*aNewHandle = id;
return true;
}
+1 -1
View File
@@ -79,7 +79,7 @@ if CONFIG['OS_TARGET'] == 'Android':
elif CONFIG['OS_ARCH'] == 'Darwin':
EXPORTS.mozilla.ipc += ['SharedMemoryBasic_mach.h']
SOURCES += [
'SharedMemoryBasic_mach.cpp',
'SharedMemoryBasic_mach.mm',
]
else:
EXPORTS.mozilla.ipc += ['SharedMemoryBasic_chromium.h']
+1 -1
View File
@@ -74,7 +74,7 @@ protected:
virtual PTestBridgeMainSubChild*
AllocPTestBridgeMainSubChild(Transport* transport,
ProcessId otherProcess) MOZ_OVERRIDE
ProcessId otherProcess) override
{
// This shouldn't be called. It's just a byproduct of testing that
// the right code is generated for a bridged protocol that's also
+132 -54
View File
@@ -8,10 +8,17 @@ loadRelativeToScript('CFG.js');
var sourceRoot = (os.getenv('SOURCE') || '') + '/'
var functionName;
var functionBodies;
if (typeof scriptArgs[0] != 'string' || typeof scriptArgs[1] != 'string')
throw "Usage: analyzeRoots.js <gcFunctions.lst> <gcEdges.txt> <suppressedFunctions.lst> <gcTypes.txt> [start end [tmpfile]]";
throw "Usage: analyzeRoots.js [-f function_name] <gcFunctions.lst> <gcEdges.txt> <suppressedFunctions.lst> <gcTypes.txt> [start end [tmpfile]]";
var theFunctionNameToFind;
if (scriptArgs[0] == '--function') {
theFunctionNameToFind = scriptArgs[1];
scriptArgs = scriptArgs.slice(2);
}
var gcFunctionsFile = scriptArgs[0];
var gcEdgesFile = scriptArgs[1];
@@ -85,6 +92,21 @@ function expressionUsesVariable(exp, variable)
return false;
}
function expressionUsesVariableContents(exp, variable)
{
if (!("Exp" in exp))
return false;
for (var childExp of exp.Exp) {
if (childExp.Kind == 'Drf') {
if (expressionUsesVariable(childExp, variable))
return true;
} else if (expressionUsesVariableContents(childExp, variable)) {
return true;
}
}
return false;
}
// Detect simple |return nullptr;| statements.
function isReturningImmobileValue(edge, variable)
{
@@ -128,7 +150,7 @@ function edgeUsesVariable(edge, variable, body)
return expressionUsesVariable(edge.Exp[1], variable) ? src : 0;
case "Assume":
return expressionUsesVariable(edge.Exp[0], variable) ? src : 0;
return expressionUsesVariableContents(edge.Exp[0], variable) ? src : 0;
case "Call":
if (expressionUsesVariable(edge.Exp[0], variable))
@@ -186,7 +208,7 @@ function edgeTakesVariableAddress(edge, variable)
function edgeKillsVariable(edge, variable)
{
// Direct assignments kill their lhs.
// Direct assignments kill their lhs: var = value
if (edge.Kind == "Assign") {
var lhs = edge.Exp[0];
if (lhs.Kind == "Var" && sameVariable(lhs.Variable, variable))
@@ -250,49 +272,78 @@ function edgeCanGC(edge)
{
if (edge.Kind != "Call")
return false;
var callee = edge.Exp[0];
while (callee.Kind == "Drf")
callee = callee.Exp[0];
if (callee.Kind == "Var") {
var variable = callee.Variable;
assert(variable.Kind == "Func");
var callee = mangled(variable.Name[0]);
if ((callee in gcFunctions) || ((callee + internalMarker) in gcFunctions))
return "'" + variable.Name[0] + "'";
return null;
if (variable.Kind == "Func") {
var callee = mangled(variable.Name[0]);
if ((callee in gcFunctions) || ((callee + internalMarker) in gcFunctions))
return "'" + variable.Name[0] + "'";
return null;
}
var varName = variable.Name[0];
return indirectCallCannotGC(functionName, varName) ? null : "*" + varName;
}
assert(callee.Kind == "Drf");
if (callee.Exp[0].Kind == "Fld") {
var field = callee.Exp[0].Field;
if (callee.Kind == "Fld") {
var field = callee.Field;
var csuName = field.FieldCSU.Type.Name;
var fullFieldName = csuName + "." + field.Name[0];
if (fieldCallCannotGC(csuName, fullFieldName))
return null;
return (fullFieldName in suppressedFunctions) ? null : fullFieldName;
}
assert(callee.Exp[0].Kind == "Var");
var varName = callee.Exp[0].Variable.Name[0];
return indirectCallCannotGC(functionName, varName) ? null : "*" + varName;
}
function variableUsePrecedesGC(suppressed, variable, worklist)
// Search recursively through predecessors from a variable use, returning
// whether a GC call is reachable (in the reverse direction; this means that
// the variable use is reachable from the GC call, and therefore the variable
// is live after the GC call), along with some additional information. What
// info we want depends on whether the variable turns out to be live across any
// GC call. We are looking for both hazards (unrooted variables live across GC
// calls) and unnecessary roots (rooted variables that have no GC calls in
// their live ranges.)
//
// If not:
//
// - 'minimumUse': the earliest point in each body that uses the variable, for
// reporting on unnecessary roots.
//
// If so:
//
// - 'why': a path from the GC call to a use of the variable after the GC
// call, chained through a 'why' field in the returned edge descriptor
//
// - 'gcInfo': a direct pointer to the GC call edge
//
function findGCBeforeVariableUse(suppressed, variable, worklist)
{
// Scan through all edges preceding an unrooted variable use, using an
// explicit worklist. A worklist contains an incoming edge together with a
// description of where it or one of its successors GC'd (if any).
// explicit worklist, looking for a GC call. A worklist contains an
// incoming edge together with a description of where it or one of its
// successors GC'd (if any).
while (worklist.length) {
var entry = worklist.pop();
var body = entry.body, ppoint = entry.ppoint;
var { body, ppoint, gcInfo } = entry;
if (body.seen) {
if (ppoint in body.seen) {
var seenEntry = body.seen[ppoint];
if (!entry.gcInfo || seenEntry.gcInfo)
if (!gcInfo || seenEntry.gcInfo)
continue;
}
} else {
body.seen = [];
}
body.seen[ppoint] = {body:body, gcInfo:entry.gcInfo};
body.seen[ppoint] = {body: body, gcInfo: gcInfo};
if (ppoint == body.Index[0]) {
if (body.BlockId.Kind == "Loop") {
@@ -304,17 +355,17 @@ function variableUsePrecedesGC(suppressed, variable, worklist)
if (sameBlockId(xbody.BlockId, parent.BlockId)) {
assert(!found);
found = true;
worklist.push({body:xbody, ppoint:parent.Index,
gcInfo:entry.gcInfo, why:entry});
worklist.push({body: xbody, ppoint: parent.Index,
gcInfo: gcInfo, why: entry});
}
}
assert(found);
}
}
} else if (variable.Kind == "Arg" && entry.gcInfo) {
} else if (variable.Kind == "Arg" && gcInfo) {
// The scope of arguments starts at the beginning of the
// function
return {gcInfo:entry.gcInfo, why:entry};
return {gcInfo: gcInfo, why: entry};
}
}
@@ -325,31 +376,45 @@ function variableUsePrecedesGC(suppressed, variable, worklist)
for (var edge of predecessors[ppoint]) {
var source = edge.Index[0];
if (edgeKillsVariable(edge, variable)) {
if (entry.gcInfo)
return {gcInfo: entry.gcInfo, why: {body:body, ppoint:source, gcInfo:entry.gcInfo, why:entry } }
var edge_kills = edgeKillsVariable(edge, variable);
var edge_uses = edgeUsesVariable(edge, variable, body);
if (edge_kills || edge_uses) {
if (!body.minimumUse || source < body.minimumUse)
body.minimumUse = source;
}
if (edge_kills) {
// This is a beginning of the variable's live range. If we can
// reach a GC call from here, then we're done -- we have a path
// from the beginning of the live range, through the GC call,
// to a use after the GC call that proves its live range
// extends at least that far.
if (gcInfo)
return {gcInfo: gcInfo, why: {body: body, ppoint: source, gcInfo: gcInfo, why: entry } }
// Otherwise, we want to continue searching for the true
// minimumUse, for use in reporting unnecessary rooting, but we
// truncate this particular branch of the search at this edge.
continue;
}
var gcInfo = entry.gcInfo;
if (!gcInfo && !(source in body.suppressed) && !suppressed) {
var gcName = edgeCanGC(edge, body);
if (gcName)
gcInfo = {name:gcName, body:body, ppoint:source};
}
if (edgeUsesVariable(edge, variable, body)) {
if (edge_uses) {
// The live range starts at least this far back, so we're done
// for the same reason as with edge_kills.
if (gcInfo)
return {gcInfo:gcInfo, why:entry};
if (!body.minimumUse || source < body.minimumUse)
body.minimumUse = source;
}
if (edge.Kind == "Loop") {
// propagate to exit points of the loop body, in addition to the
// predecessor of the loop edge itself.
// Additionally propagate the search into a loop body, starting
// with the exit point.
var found = false;
for (var xbody of functionBodies) {
if (sameBlockId(xbody.BlockId, edge.BlockId)) {
@@ -362,6 +427,8 @@ function variableUsePrecedesGC(suppressed, variable, worklist)
assert(found);
break;
}
// Propagate the search to the predecessors of this edge.
worklist.push({body:body, ppoint:source, gcInfo:gcInfo, why:entry});
}
}
@@ -384,14 +451,21 @@ function variableLiveAcrossGC(suppressed, variable)
continue;
for (var edge of body.PEdge) {
var usePoint = edgeUsesVariable(edge, variable, body);
// Example for !edgeKillsVariable:
//
// JSObject* obj = NewObject();
// cangc();
// obj = NewObject(); <-- uses 'obj', but kills previous value
//
if (usePoint && !edgeKillsVariable(edge, variable)) {
// Found a use, possibly after a GC.
var worklist = [{body:body, ppoint:usePoint, gcInfo:null, why:null}];
var call = variableUsePrecedesGC(suppressed, variable, worklist);
if (call) {
call.afterGCUse = usePoint;
return call;
}
var call = findGCBeforeVariableUse(suppressed, variable, worklist);
if (!call)
continue;
call.afterGCUse = usePoint;
return call;
}
}
}
@@ -612,24 +686,10 @@ var end = Math.min(minStream + each * batch - 1, maxStream);
var theFunctionNameToFind;
// var start = end = 12345;
for (var nameIndex = start; nameIndex <= end; nameIndex++) {
var name = xdb.read_key(nameIndex);
var functionName = name.readString();
var data = xdb.read_entry(name);
xdb.free_string(name);
var json = data.readString();
xdb.free_string(data);
function process(name, json) {
functionName = name;
functionBodies = JSON.parse(json);
if (theFunctionNameToFind) {
if (functionName == theFunctionNameToFind) {
printErr("nameIndex = " + nameIndex);
quit(1);
} else {
continue;
}
}
for (var body of functionBodies)
body.suppressed = [];
for (var body of functionBodies) {
@@ -638,3 +698,21 @@ for (var nameIndex = start; nameIndex <= end; nameIndex++) {
}
processBodies(functionName);
}
if (theFunctionNameToFind) {
var data = xdb.read_entry(theFunctionNameToFind);
var json = data.readString();
process(theFunctionNameToFind, json);
xdb.free_string(data);
quit(0);
}
for (var nameIndex = start; nameIndex <= end; nameIndex++) {
var name = xdb.read_key(nameIndex);
var functionName = name.readString();
var data = xdb.read_entry(name);
xdb.free_string(name);
var json = data.readString();
process(functionName, json);
xdb.free_string(data);
}
+25 -16
View File
@@ -244,21 +244,6 @@ function ignoreGCFunction(mangled)
return false;
}
function isRootedTypeName(name)
{
if (name == "mozilla::ErrorResult" ||
name == "JSErrorResult" ||
name == "WrappableJSErrorResult" ||
name == "js::frontend::TokenStream" ||
name == "js::frontend::TokenStream::Position" ||
name == "ModuleCompiler" ||
name == "JSAddonId")
{
return true;
}
return false;
}
function stripUCSAndNamespace(name)
{
if (name.startsWith('struct '))
@@ -281,16 +266,36 @@ function stripUCSAndNamespace(name)
return name;
}
function isRootedPointerTypeName(name)
function isRootedGCTypeName(name)
{
return (name == "JSAddonId");
}
function isRootedGCPointerTypeName(name)
{
name = stripUCSAndNamespace(name);
if (name.startsWith('MaybeRooted<'))
return /\(js::AllowGC\)1u>::RootType/.test(name);
if (name == "ErrorResult" ||
name == "JSErrorResult" ||
name == "WrappableJSErrorResult" ||
name == "frontend::TokenStream" ||
name == "frontend::TokenStream::Position" ||
name == "ModuleCompiler")
{
return true;
}
return name.startsWith('Rooted') || name.startsWith('PersistentRooted');
}
function isRootedTypeName(name)
{
return isRootedGCTypeName(name) || isRootedGCPointerTypeName(name);
}
function isUnsafeStorage(typeName)
{
typeName = stripUCSAndNamespace(typeName);
@@ -360,6 +365,10 @@ function listGCPointers() {
'JS::Value',
'jsid',
'js::TypeSet',
'js::TypeSet::ObjectKey',
'js::TypeSet::Type',
// AutoCheckCannotGC should also not be held live across a GC function.
'JS::AutoCheckCannotGC',
];
+49 -29
View File
@@ -69,44 +69,60 @@ var gcTypes = {}; // map from parent struct => Set of GC typed children
var gcPointers = {}; // map from parent struct => Set of GC typed children
var nonGCTypes = {}; // set of types that would ordinarily be GC types but we are suppressing
var nonGCPointers = {}; // set of types that would ordinarily be GC pointers but we are suppressing
var gcFields = {};
var gcFields = new Map;
// "typeName is a (pointer to a)*'depth' GC type because it contains a field
// named 'child' of type 'why' (or pointer to 'why' if ptrdness == 1), which
function stars(n) { return n ? '*' + stars(n-1) : '' };
// "typeName is a (pointer to a)^'typePtrLevel' GC type because it contains a field
// named 'child' of type 'why' (or pointer to 'why' if fieldPtrLevel == 1), which is
// itself a GCThing or GCPointer."
function markGCType(typeName, child, why, depth, ptrdness)
function markGCType(typeName, child, why, typePtrLevel, fieldPtrLevel, indent)
{
//printErr(`${indent}${typeName}${stars(typePtrLevel)} may be a gctype/ptr because of its child '${child}' of type ${why}${stars(fieldPtrLevel)}`);
// Some types, like UniquePtr, do not mark/trace/relocate their contained
// pointers and so should not hold them live across a GC. UniquePtr in
// particular should be the only thing pointing to a structure containing a
// GCPointer, so nothing else can be tracing it and it'll die when the
// UniquePtr goes out of scope. So we say that a UniquePtr's memory is just
// as unsafe as the stack for storing GC pointers.
if (!ptrdness && isUnsafeStorage(typeName)) {
// GCPointer, so nothing else can possibly trace it and it'll die when the
// UniquePtr goes out of scope. So we say that memory pointed to by a
// UniquePtr is just as unsafe as the stack for storing GC pointers.
if (!fieldPtrLevel && isUnsafeStorage(typeName)) {
// The UniquePtr itself is on the stack but when you dereference the
// contained pointer, you get to the unsafe memory that we are treating
// as if it were the stack (aka depth 0). Note that
// as if it were the stack (aka ptrLevel 0). Note that
// UniquePtr<UniquePtr<JSObject*>> is fine, so we don't want to just
// hardcode the depth.
ptrdness = -1;
// hardcode the ptrLevel.
fieldPtrLevel = -1;
}
depth += ptrdness;
if (depth > 2)
// Example: with:
// struct Pair { JSObject* foo; int bar; };
// struct { Pair** info }***
// make a call to:
// child='info' typePtrLevel=3 fieldPtrLevel=2
// for a final ptrLevel of 5, used to later call:
// child='foo' typePtrLevel=5 fieldPtrLevel=1
//
var ptrLevel = typePtrLevel + fieldPtrLevel;
// ...except when > 2 levels of pointers away from an actual GC thing, stop
// searching the graph. (This would just be > 1, except that a UniquePtr
// field might still have a GC pointer.)
if (ptrLevel > 2)
return;
if (depth == 0 && isRootedTypeName(typeName))
if (ptrLevel == 0 && isRootedGCTypeName(typeName))
return;
if (depth == 1 && isRootedPointerTypeName(typeName))
if (ptrLevel == 1 && isRootedGCPointerTypeName(typeName))
return;
if (depth == 0) {
if (ptrLevel == 0) {
if (typeName in nonGCTypes)
return;
if (!(typeName in gcTypes))
gcTypes[typeName] = new Set();
gcTypes[typeName].add(why);
} else if (depth == 1) {
} else if (ptrLevel == 1) {
if (typeName in nonGCPointers)
return;
if (!(typeName in gcPointers))
@@ -114,32 +130,34 @@ function markGCType(typeName, child, why, depth, ptrdness)
gcPointers[typeName].add(why);
}
if (!(typeName in gcFields))
gcFields[typeName] = new Map();
gcFields[typeName].set(why, [ child, ptrdness ]);
if (ptrLevel < 2) {
if (!gcFields.has(typeName))
gcFields.set(typeName, new Map());
gcFields.get(typeName).set(child, [ why, fieldPtrLevel ]);
}
if (typeName in structureParents) {
for (var field of structureParents[typeName]) {
var [ holderType, fieldName ] = field;
markGCType(holderType, typeName, fieldName, depth, 0);
markGCType(holderType, fieldName, typeName, ptrLevel, 0, indent + " ");
}
}
if (typeName in pointerParents) {
for (var field of pointerParents[typeName]) {
var [ holderType, fieldName ] = field;
markGCType(holderType, typeName, fieldName, depth, 1);
markGCType(holderType, fieldName, typeName, ptrLevel, 1, indent + " ");
}
}
}
function addGCType(typeName, child, why, depth, ptrdness)
function addGCType(typeName, child, why, depth, fieldPtrLevel)
{
markGCType(typeName, 'annotation', '<annotation>', 0, 0);
markGCType(typeName, '<annotation>', '(annotation)', 0, 0, "");
}
function addGCPointer(typeName)
{
markGCType(typeName, 'annotation', '<pointer-annotation>', 1, 0);
markGCType(typeName, '<pointer-annotation>', '(annotation)', 1, 0, "");
}
for (var type of listNonGCTypes())
@@ -158,17 +176,19 @@ function explain(csu, indent, seen) {
if (!seen)
seen = new Set();
seen.add(csu);
if (!(csu in gcFields))
if (!gcFields.has(csu))
return;
if (gcFields[csu].has('<annotation>')) {
var fields = gcFields.get(csu);
if (fields.has('<annotation>')) {
print(indent + "which is a GCThing because I said so");
return;
}
if (gcFields[csu].has('<pointer-annotation>')) {
if (fields.has('<pointer-annotation>')) {
print(indent + "which is a GCPointer because I said so");
return;
}
for (var [ field, [ child, ptrdness ] ] of gcFields[csu]) {
for (var [ field, [ child, ptrdness ] ] of fields) {
var inherit = "";
if (field == "field:0")
inherit = " (probably via inheritance)";
-2
View File
@@ -7525,8 +7525,6 @@ BytecodeEmitter::emitTree(ParseNode* pn)
ParseNode* rest = nullptr;
bool restIsDefn = false;
if (fun->hasRest() && hasDefaults) {
MOZ_ASSERT(!sc->asFunctionBox()->argumentsHasLocalBinding());
// Defaults with a rest parameter need special handling. The
// rest parameter needs to be undefined while defaults are being
// processed. To do this, we create the rest argument and let it
File diff suppressed because it is too large Load Diff
+5
View File
@@ -408,6 +408,9 @@ class Parser : private JS::AutoGCRooter, public StrictModeGetter
/* State specific to the kind of parse being performed. */
ParseHandler handler;
void prepareNodeForMutation(Node node) { handler.prepareNodeForMutation(node); }
void freeTree(Node node) { handler.freeTree(node); }
private:
bool reportHelper(ParseReportKind kind, bool strict, uint32_t offset,
unsigned errorNumber, va_list args);
@@ -732,6 +735,7 @@ class Parser : private JS::AutoGCRooter, public StrictModeGetter
ParseNodeKind headKind);
bool checkForHeadConstInitializers(Node pn1);
public:
enum FunctionCallBehavior {
PermitAssignmentToFunctionCalls,
ForbidAssignmentToFunctionCalls
@@ -740,6 +744,7 @@ class Parser : private JS::AutoGCRooter, public StrictModeGetter
bool isValidSimpleAssignmentTarget(Node node,
FunctionCallBehavior behavior = ForbidAssignmentToFunctionCalls);
private:
bool reportIfArgumentsEvalTarget(Node nameNode);
bool reportIfNotValidSimpleAssignmentTarget(Node target, AssignmentFlavor flavor);
+3
View File
@@ -172,6 +172,9 @@ class SyntaxParseHandler
static Node null() { return NodeFailure; }
void prepareNodeForMutation(Node node) {}
void freeTree(Node node) {}
void trace(JSTracer* trc) {}
Node newName(PropertyName* name, uint32_t blockid, const TokenPos& pos, ExclusiveContext* cx) {
+3
View File
@@ -0,0 +1,3 @@
// |jit-test| --no-ggc; allow-unhandlable-oom
load(libdir + 'oomTest.js');
oomTest((function(x) { assertEq(x + y + ex, 25); }));
+14 -9
View File
@@ -221,19 +221,24 @@ jit::ExceptionHandlerBailout(JSContext* cx, const InlineFrameIterator& frame,
rfe->target = cx->runtime()->jitRuntime()->getBailoutTail()->raw();
rfe->bailoutInfo = bailoutInfo;
} else {
// Bailout failed. If there was a fatal error, clear the
// exception to turn this into an uncatchable error. If the
// overrecursion check failed, continue popping all inline
// frames and have the caller report an overrecursion error.
// Bailout failed. If the overrecursion check failed, clear the
// exception to turn this into an uncatchable error, continue popping
// all inline frames and have the caller report the error.
MOZ_ASSERT(!bailoutInfo);
if (!excInfo.propagatingIonExceptionForDebugMode())
cx->clearPendingException();
if (retval == BAILOUT_RETURN_OVERRECURSED)
if (retval == BAILOUT_RETURN_OVERRECURSED) {
*overrecursed = true;
else
if (!excInfo.propagatingIonExceptionForDebugMode())
cx->clearPendingException();
} else {
MOZ_ASSERT(retval == BAILOUT_RETURN_FATAL_ERROR);
// Crash for now so as not to complicate the exception handling code
// further.
if (cx->isThrowingOutOfMemory())
CrashAtUnhandlableOOM("ExceptionHandlerBailout");
MOZ_CRASH();
}
}
// Make the frame being bailed out the top profiled frame.
+3 -1
View File
@@ -1471,8 +1471,10 @@ jit::BailoutIonToBaseline(JSContext* cx, JitActivation* activation, JitFrameIter
// Allocate buffer to hold stack replacement data.
BaselineStackBuilder builder(iter, 1024);
if (!builder.init())
if (!builder.init()) {
ReportOutOfMemory(cx);
return BAILOUT_RETURN_FATAL_ERROR;
}
JitSpew(JitSpew_BaselineBailouts, " Incoming frame ptr = %p", builder.startFrame());
SnapshotIteratorForBailout snapIter(activation, iter);
@@ -460,6 +460,10 @@ struct AssemblerBufferWithConstantPools : public AssemblerBuffer<SliceSize, Inst
// MacroAssembler before allocating any space.
void initWithAllocator() {
poolInfo_ = this->lifoAlloc_.template newArrayUninitialized<PoolInfo>(poolInfoSize_);
if (!poolInfo_) {
this->fail_oom();
return;
}
new (&pool_) Pool (poolMaxOffset_, pcBias_, this->lifoAlloc_);
if (pool_.poolData() == nullptr)
@@ -0,0 +1,41 @@
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/licenses/publicdomain/
//-----------------------------------------------------------------------------
var BUGNUMBER = 1183400;
var summary =
"Deletion of a && or || expression that constant-folds to a name must not " +
"attempt to delete the name";
print(BUGNUMBER + ": " + summary);
/**************
* BEGIN TEST *
**************/
Object.defineProperty(this, "nonconfigurable", { value: 42 });
assertEq(nonconfigurable, 42);
assertEq(delete nonconfigurable, false);
assertEq(delete (true && nonconfigurable), true);
function nested()
{
assertEq(delete nonconfigurable, false);
assertEq(delete (true && nonconfigurable), true);
}
nested();
function nestedStrict()
{
"use strict";
assertEq(delete (true && nonconfigurable), true);
}
nestedStrict();
/******************************************************************************/
if (typeof reportCompare === "function")
reportCompare(true, true);
print("Tests complete");
@@ -0,0 +1,28 @@
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/licenses/publicdomain/
//-----------------------------------------------------------------------------
var BUGNUMBER = 1182373;
var summary =
"Don't let constant-folding in the MemberExpression part of a tagged " +
"template cause an incorrect |this| be passed to the callee";
print(BUGNUMBER + ": " + summary);
/**************
* BEGIN TEST *
**************/
var prop = "global";
var obj = { prop: "obj", f: function() { return this.prop; } };
assertEq(obj.f``, "obj");
assertEq((true ? obj.f : null)``, "global");
/******************************************************************************/
if (typeof reportCompare === "function")
reportCompare(true, true);
print("Tests complete");
@@ -0,0 +1,35 @@
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/licenses/publicdomain/
//-----------------------------------------------------------------------------
var gTestfile = "if-constant-folding.js";
var BUGNUMBER = 1183400;
var summary =
"Don't crash constant-folding an |if| governed by a truthy constant, whose " +
"alternative statement is another |if|";
print(BUGNUMBER + ": " + summary);
/**************
* BEGIN TEST *
**************/
// Perform |if| constant folding correctly when the condition is constantly
// truthy and the alternative statement is another |if|.
if (true)
{
assertEq(true, true, "sanity");
}
else if (42)
{
assertEq(false, true, "not reached");
assertEq(true, false, "also not reached");
}
/******************************************************************************/
if (typeof reportCompare === "function")
reportCompare(true, true);
print("Tests complete");
+2 -2
View File
@@ -505,7 +505,7 @@ void nsBidi::GetDirProps(const char16_t *aText)
} else if (state == SEEKING_STRONG_FOR_FSI) {
if (stackLast <= NSBIDI_MAX_EXPLICIT_LEVEL) {
dirProps[isolateStartStack[stackLast]] = LRI;
flags |= LRI;
flags |= DIRPROP_FLAG(LRI);
}
state = LOOKING_FOR_PDI;
}
@@ -518,7 +518,7 @@ void nsBidi::GetDirProps(const char16_t *aText)
} else if (state == SEEKING_STRONG_FOR_FSI) {
if (stackLast <= NSBIDI_MAX_EXPLICIT_LEVEL) {
dirProps[isolateStartStack[stackLast]] = RLI;
flags |= RLI;
flags |= DIRPROP_FLAG(RLI);
}
state = LOOKING_FOR_PDI;
}
+19 -25
View File
@@ -4134,39 +4134,33 @@ IntrinsicSizeOffsets(nsIFrame* aFrame, bool aForISize)
nsIFrame::IntrinsicISizeOffsetData result;
WritingMode wm = aFrame->GetWritingMode();
const nsStyleMargin* styleMargin = aFrame->StyleMargin();
bool orthogonal = aForISize == wm.IsVertical();
AddCoord(orthogonal ? styleMargin->mMargin.GetTop()
: styleMargin->mMargin.GetLeft(),
bool verticalAxis = aForISize == wm.IsVertical();
AddCoord(verticalAxis ? styleMargin->mMargin.GetTop()
: styleMargin->mMargin.GetLeft(),
aFrame, &result.hMargin, &result.hPctMargin,
false);
AddCoord(orthogonal ? styleMargin->mMargin.GetBottom()
: styleMargin->mMargin.GetRight(),
AddCoord(verticalAxis ? styleMargin->mMargin.GetBottom()
: styleMargin->mMargin.GetRight(),
aFrame, &result.hMargin, &result.hPctMargin,
false);
const nsStylePadding* stylePadding = aFrame->StylePadding();
AddCoord(orthogonal ? stylePadding->mPadding.GetTop()
: stylePadding->mPadding.GetLeft(),
AddCoord(verticalAxis ? stylePadding->mPadding.GetTop()
: stylePadding->mPadding.GetLeft(),
aFrame, &result.hPadding, &result.hPctPadding,
true);
AddCoord(orthogonal ? stylePadding->mPadding.GetBottom()
: stylePadding->mPadding.GetRight(),
AddCoord(verticalAxis ? stylePadding->mPadding.GetBottom()
: stylePadding->mPadding.GetRight(),
aFrame, &result.hPadding, &result.hPctPadding,
true);
if (aForISize) {
const nsStyleBorder* styleBorder = aFrame->StyleBorder();
result.hBorder += styleBorder->GetComputedBorderWidth(
wm.PhysicalSideForInlineAxis(eLogicalEdgeStart));
result.hBorder += styleBorder->GetComputedBorderWidth(
wm.PhysicalSideForInlineAxis(eLogicalEdgeEnd));
const nsStyleBorder* styleBorder = aFrame->StyleBorder();
if (verticalAxis) {
result.hBorder += styleBorder->GetComputedBorderWidth(NS_SIDE_TOP);
result.hBorder += styleBorder->GetComputedBorderWidth(NS_SIDE_BOTTOM);
} else {
auto wm = aFrame->StyleVisibility()->mWritingMode;
const nsStyleBorder* styleBorder = aFrame->StyleBorder();
result.hBorder += styleBorder->GetComputedBorderWidth(
WritingMode::PhysicalSideForBlockAxis(wm, eLogicalEdgeStart));
result.hBorder += styleBorder->GetComputedBorderWidth(
WritingMode::PhysicalSideForBlockAxis(wm, eLogicalEdgeEnd));
result.hBorder += styleBorder->GetComputedBorderWidth(NS_SIDE_LEFT);
result.hBorder += styleBorder->GetComputedBorderWidth(NS_SIDE_RIGHT);
}
const nsStyleDisplay* disp = aFrame->StyleDisplay();
@@ -4178,16 +4172,16 @@ IntrinsicSizeOffsets(nsIFrame* aFrame, bool aForISize)
aFrame, disp->mAppearance,
&border);
result.hBorder =
presContext->DevPixelsToAppUnits(orthogonal ? border.TopBottom()
: border.LeftRight());
presContext->DevPixelsToAppUnits(verticalAxis ? border.TopBottom()
: border.LeftRight());
nsIntMargin padding;
if (presContext->GetTheme()->GetWidgetPadding(presContext->DeviceContext(),
aFrame, disp->mAppearance,
&padding)) {
result.hPadding =
presContext->DevPixelsToAppUnits(orthogonal ? padding.TopBottom()
: padding.LeftRight());
presContext->DevPixelsToAppUnits(verticalAxis ? padding.TopBottom()
: padding.LeftRight());
result.hPctPadding = 0;
}
}
+13 -7
View File
@@ -37,7 +37,7 @@ interface nsIDOMWindow;
interface nsIPermission;
interface nsISimpleEnumerator;
[scriptable, uuid(93a156f8-bcc8-4568-a214-389b073332dd)]
[scriptable, uuid(0d1b8c65-0359-4a8c-b94d-4d3643b23e61)]
interface nsIPermissionManager : nsISupports
{
/**
@@ -106,17 +106,16 @@ interface nsIPermissionManager : nsISupports
[optional] in int64_t expireTime);
/**
* Remove permission information for a given host string and permission type.
* The host string represents the exact entry in the permission list (such as
* obtained from the enumerator), not a URI which that permission might apply
* to.
* Remove permission information for a given URI and permission type. This will
* remove the permission for the entire host described by the uri, acting as the
* opposite operation to the add() method.
*
* @param host the host to remove the permission for
* @param uri the uri to remove the permission for
* @param type a case-sensitive ASCII string, identifying the consumer.
* The type must have been previously registered using the
* add() method.
*/
void remove(in AUTF8String host,
void remove(in nsIURI uri,
in string type);
/**
@@ -127,6 +126,13 @@ interface nsIPermissionManager : nsISupports
*/
void removeFromPrincipal(in nsIPrincipal principal, in string type);
/**
* Remove the given permission from the permission manager.
*
* @param perm a permission obtained from the permission manager.
*/
void removePermission(in nsIPermission perm);
/**
* Clear permission information for all websites.
*/
+14 -14
View File
@@ -48,7 +48,7 @@ this.LoginHelper = {
aHostname.indexOf("\r") != -1 ||
aHostname.indexOf("\n") != -1 ||
aHostname.indexOf("\0") != -1) {
throw "Invalid hostname";
throw new Error("Invalid hostname");
}
},
@@ -73,7 +73,7 @@ this.LoginHelper = {
// Nulls are invalid, as they don't round-trip well.
// Mostly not a formatting problem, although ".\0" can be quirky.
if (badCharacterPresent(aLogin, "\0")) {
throw "login values can't contain nulls";
throw new Error("login values can't contain nulls");
}
// In theory these nulls should just be rolled up into the encrypted
@@ -82,26 +82,26 @@ this.LoginHelper = {
// unexpected round-trip surprises.
if (aLogin.username.indexOf("\0") != -1 ||
aLogin.password.indexOf("\0") != -1) {
throw "login values can't contain nulls";
throw new Error("login values can't contain nulls");
}
// Newlines are invalid for any field stored as plaintext.
if (badCharacterPresent(aLogin, "\r") ||
badCharacterPresent(aLogin, "\n")) {
throw "login values can't contain newlines";
throw new Error("login values can't contain newlines");
}
// A line with just a "." can have special meaning.
if (aLogin.usernameField == "." ||
aLogin.formSubmitURL == ".") {
throw "login values can't be periods";
throw new Error("login values can't be periods");
}
// A hostname with "\ \(" won't roundtrip.
// eg host="foo (", realm="bar" --> "foo ( (bar)"
// vs host="foo", realm=" (bar" --> "foo ( (bar)"
if (aLogin.hostname.indexOf(" (") != -1) {
throw "bad parens in hostname";
throw new Error("bad parens in hostname");
}
},
@@ -189,40 +189,40 @@ this.LoginHelper = {
// Fail if caller requests setting an unknown property.
default:
throw "Unexpected propertybag item: " + prop.name;
throw new Error("Unexpected propertybag item: " + prop.name);
}
}
} else {
throw "newLoginData needs an expected interface!";
throw new Error("newLoginData needs an expected interface!");
}
// Sanity check the login
if (newLogin.hostname == null || newLogin.hostname.length == 0) {
throw "Can't add a login with a null or empty hostname.";
throw new Error("Can't add a login with a null or empty hostname.");
}
// For logins w/o a username, set to "", not null.
if (newLogin.username == null) {
throw "Can't add a login with a null username.";
throw new Error("Can't add a login with a null username.");
}
if (newLogin.password == null || newLogin.password.length == 0) {
throw "Can't add a login with a null or empty password.";
throw new Error("Can't add a login with a null or empty password.");
}
if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
// We have a form submit URL. Can't have a HTTP realm.
if (newLogin.httpRealm != null) {
throw "Can't add a login with both a httpRealm and formSubmitURL.";
throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
}
} else if (newLogin.httpRealm) {
// We have a HTTP realm. Can't have a form submit URL.
if (newLogin.formSubmitURL != null) {
throw "Can't add a login with both a httpRealm and formSubmitURL.";
throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
}
} else {
// Need one or the other!
throw "Can't add a login without a httpRealm or formSubmitURL.";
throw new Error("Can't add a login without a httpRealm or formSubmitURL.");
}
// Throws if there are bogus values.
@@ -8,16 +8,16 @@
this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
"UserAutoCompleteResult" ];
const Ci = Components.interfaces;
const Cr = Components.results;
const Cc = Components.classes;
const Cu = Components.utils;
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginRecipesContent",
"resource://gre/modules/LoginRecipes.jsm");
// These mirror signon.* prefs.
var gEnabled, gDebug, gAutofillForms, gStoreWhenAutocompleteOff;
@@ -165,22 +165,48 @@ var LoginManagerContent = {
},
receiveMessage: function (msg) {
// Convert an array of logins in simple JS-object form to an array of
// nsILoginInfo objects.
function jsLoginsToXPCOM(logins) {
return logins.map(login => {
var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
formLogin.init(login.hostname, login.formSubmitURL,
login.httpRealm, login.username,
login.password, login.usernameField,
login.passwordField);
return formLogin;
});
}
let request = this._takeRequest(msg);
switch (msg.name) {
case "RemoteLogins:loginsFound": {
request.promise.resolve({ form: request.form,
loginsFound: msg.data.logins });
let loginsFound = jsLoginsToXPCOM(msg.data.logins);
request.promise.resolve({
form: request.form,
loginsFound: loginsFound,
recipes: msg.data.recipes,
});
break;
}
case "RemoteLogins:loginsAutoCompleted": {
request.promise.resolve(msg.data.logins);
let loginsFound = jsLoginsToXPCOM(msg.data.logins);
request.promise.resolve(loginsFound);
break;
}
}
},
_asyncFindLogins: function(form, options) {
/**
* Get relevant logins and recipes from the parent
*
* @param {HTMLFormElement} form - form to get login data for
* @param {Object} options
* @param {boolean} options.showMasterPassword - whether to show a master password prompt
*/
_getLoginDataFromParent: function(form, options) {
let doc = form.ownerDocument;
let win = doc.defaultView;
@@ -246,16 +272,16 @@ var LoginManagerContent = {
return;
log("onFormPassword for", form.ownerDocument.documentURI);
this._asyncFindLogins(form, { showMasterPassword: true })
this._getLoginDataFromParent(form, { showMasterPassword: true })
.then(this.loginsFound.bind(this))
.then(null, Cu.reportError);
},
loginsFound: function({ form, loginsFound }) {
loginsFound: function({ form, loginsFound, recipes }) {
let doc = form.ownerDocument;
let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView);
this._fillForm(form, autofillForm, false, false, loginsFound);
this._fillForm(form, autofillForm, false, false, loginsFound, recipes);
},
/*
@@ -296,9 +322,9 @@ var LoginManagerContent = {
var [usernameField, passwordField, ignored] =
this._getFormFields(acForm, false);
if (usernameField == acInputField && passwordField) {
this._asyncFindLogins(acForm, { showMasterPassword: false })
.then(({ form, loginsFound }) => {
this._fillForm(form, true, true, true, loginsFound);
this._getLoginDataFromParent(acForm, { showMasterPassword: false })
.then(({ form, loginsFound, recipes }) => {
this._fillForm(form, true, true, true, loginsFound, recipes);
})
.then(null, Cu.reportError);
} else {
@@ -366,14 +392,15 @@ var LoginManagerContent = {
},
/*
* _getFormFields
*
/**
* Returns the username and password fields found in the form.
* Can handle complex forms by trying to figure out what the
* relevant fields are.
*
* Returns: [usernameField, newPasswordField, oldPasswordField]
* @param {HTMLFormElement} form
* @param {bool} isSubmission
* @param {Set} recipes
* @return {Array} [usernameField, newPasswordField, oldPasswordField]
*
* usernameField may be null.
* newPasswordField will always be non-null.
@@ -382,37 +409,69 @@ var LoginManagerContent = {
* change-password field, with oldPasswordField containing the password
* that is being changed.
*/
_getFormFields : function (form, isSubmission) {
_getFormFields : function (form, isSubmission, recipes) {
var usernameField = null;
var pwFields = null;
var fieldOverrideRecipe = LoginRecipesContent.getFieldOverrides(recipes, form);
if (fieldOverrideRecipe) {
var pwOverrideField = LoginRecipesContent.queryLoginField(
form,
fieldOverrideRecipe.passwordSelector
);
if (pwOverrideField) {
pwFields = [{
index : [...pwOverrideField.form.elements].indexOf(pwOverrideField),
element : pwOverrideField,
}];
}
// Locate the password field(s) in the form. Up to 3 supported.
// If there's no password field, there's nothing for us to do.
var pwFields = this._getPasswordFields(form, isSubmission);
if (!pwFields)
var usernameOverrideField = LoginRecipesContent.queryLoginField(
form,
fieldOverrideRecipe.usernameSelector
);
if (usernameOverrideField) {
usernameField = usernameOverrideField;
}
}
if (!pwFields) {
// Locate the password field(s) in the form. Up to 3 supported.
// If there's no password field, there's nothing for us to do.
pwFields = this._getPasswordFields(form, isSubmission);
}
if (!pwFields) {
return [null, null, null];
}
// Locate the username field in the form by searching backwards
// from the first passwordfield, assume the first text field is the
// username. We might not find a username field if the user is
// already logged in to the site.
for (var i = pwFields[0].index - 1; i >= 0; i--) {
var element = form.elements[i];
if (this._isUsernameFieldType(element)) {
usernameField = element;
break;
if (!usernameField) {
// Locate the username field in the form by searching backwards
// from the first passwordfield, assume the first text field is the
// username. We might not find a username field if the user is
// already logged in to the site.
for (var i = pwFields[0].index - 1; i >= 0; i--) {
var element = form.elements[i];
if (this._isUsernameFieldType(element)) {
usernameField = element;
break;
}
}
}
if (!usernameField)
log("(form -- no username field found)");
else
log("Username field id/name/value is: ", usernameField.id, " / ",
usernameField.name, " / ", usernameField.value);
// If we're not submitting a form (it's a page load), there are no
// password field values for us to use for identifying fields. So,
// just assume the first password field is the one to be filled in.
if (!isSubmission || pwFields.length == 1)
return [usernameField, pwFields[0].element, null];
if (!isSubmission || pwFields.length == 1) {
var passwordField = pwFields[0].element;
log("Password field id/name is: ", passwordField.id, " / ", passwordField.name);
return [usernameField, passwordField, null];
}
// Try to figure out WTF is in the form based on the password values.
@@ -455,6 +514,8 @@ var LoginManagerContent = {
}
}
log("Password field (new) id/name is: ", newPasswordField.id, " / ", newPasswordField.name);
log("Password field (old) id/name is: ", oldPasswordField.id, " / ", oldPasswordField.name);
return [usernameField, newPasswordField, oldPasswordField];
},
@@ -559,21 +620,21 @@ var LoginManagerContent = {
{ openerWin: opener });
},
/*
* _fillform
/**
* Attempt to find the username and password fields in a form, and fill them
* in using the provided logins and recipes.
*
* Fill the form with the provided login information.
* The logins are returned so they can be reused for
* optimization. Success of action is also returned in format
* [success, foundLogins].
*
* - autofillForm denotes if we should fill the form in automatically
* - userTriggered is an indication of whether this filling was triggered by
* the user
* - foundLogins is an array of nsILoginInfo
* @param {HTMLFormElement} form
* @param {bool} autofillForm denotes if we should fill the form in automatically
* @param {bool} clobberPassword controls if an existing password value can be
* overwritten
* @param {bool} userTriggered is an indication of whether this filling was triggered by
* the user
* @param {nsILoginInfo[]} foundLogins is an array of nsILoginInfo that could be used for the form
* @param {Set} recipes that could be used to affect how the form is filled
*/
_fillForm : function (form, autofillForm, clobberPassword,
userTriggered, foundLogins) {
userTriggered, foundLogins, recipes) {
let ignoreAutocomplete = true;
const AUTOFILL_RESULT = {
FILLED: 0,
@@ -586,7 +647,6 @@ var LoginManagerContent = {
MULTIPLE_LOGINS: 7,
NO_AUTOFILL_FORMS: 8,
AUTOCOMPLETE_OFF: 9,
UNKNOWN_FAILURE: 10,
};
function recordAutofillResult(result) {
@@ -598,118 +658,98 @@ var LoginManagerContent = {
autofillResultHist.add(result);
}
// Nothing to do if we have no matching logins available.
if (foundLogins.length == 0) {
// We don't log() here since this is a very common case.
recordAutofillResult(AUTOFILL_RESULT.NO_SAVED_LOGINS);
return [false, foundLogins];
}
try {
// Nothing to do if we have no matching logins available.
if (foundLogins.length == 0) {
// We don't log() here since this is a very common case.
recordAutofillResult(AUTOFILL_RESULT.NO_SAVED_LOGINS);
return;
}
// Heuristically determine what the user/pass fields are
// We do this before checking to see if logins are stored,
// so that the user isn't prompted for a master password
// without need.
var [usernameField, passwordField, ignored] =
this._getFormFields(form, false);
// Heuristically determine what the user/pass fields are
// We do this before checking to see if logins are stored,
// so that the user isn't prompted for a master password
// without need.
var [usernameField, passwordField, ignored] =
this._getFormFields(form, false);
// Need a valid password field to do anything.
if (passwordField == null) {
log("not filling form, no password field found");
recordAutofillResult(AUTOFILL_RESULT.NO_PASSWORD_FIELD);
return [false, foundLogins];
}
// Need a valid password field to do anything.
if (passwordField == null) {
log("not filling form, no password field found");
recordAutofillResult(AUTOFILL_RESULT.NO_PASSWORD_FIELD);
return;
}
// If the password field is disabled or read-only, there's nothing to do.
if (passwordField.disabled || passwordField.readOnly) {
log("not filling form, password field disabled or read-only");
recordAutofillResult(AUTOFILL_RESULT.PASSWORD_DISABLED_READONLY);
return [false, foundLogins];
}
// If the password field is disabled or read-only, there's nothing to do.
if (passwordField.disabled || passwordField.readOnly) {
log("not filling form, password field disabled or read-only");
recordAutofillResult(AUTOFILL_RESULT.PASSWORD_DISABLED_READONLY);
return;
}
// Discard logins which have username/password values that don't
// fit into the fields (as specified by the maxlength attribute).
// The user couldn't enter these values anyway, and it helps
// with sites that have an extra PIN to be entered (bug 391514)
var maxUsernameLen = Number.MAX_VALUE;
var maxPasswordLen = Number.MAX_VALUE;
var isAutocompleteOff = false;
if (this._isAutocompleteDisabled(form) ||
this._isAutocompleteDisabled(usernameField) ||
this._isAutocompleteDisabled(passwordField)) {
isAutocompleteOff = true;
}
// If attribute wasn't set, default is -1.
if (usernameField && usernameField.maxLength >= 0)
maxUsernameLen = usernameField.maxLength;
if (passwordField.maxLength >= 0)
maxPasswordLen = passwordField.maxLength;
// Discard logins which have username/password values that don't
// fit into the fields (as specified by the maxlength attribute).
// The user couldn't enter these values anyway, and it helps
// with sites that have an extra PIN to be entered (bug 391514)
var maxUsernameLen = Number.MAX_VALUE;
var maxPasswordLen = Number.MAX_VALUE;
foundLogins = foundLogins.map(login => {
var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
formLogin.init(login.hostname, login.formSubmitURL,
login.httpRealm, login.username,
login.password, login.usernameField,
login.passwordField);
return formLogin;
});
var logins = foundLogins.filter(function (l) {
var fit = (l.username.length <= maxUsernameLen &&
l.password.length <= maxPasswordLen);
if (!fit)
log("Ignored", l.username, "login: won't fit");
// If attribute wasn't set, default is -1.
if (usernameField && usernameField.maxLength >= 0)
maxUsernameLen = usernameField.maxLength;
if (passwordField.maxLength >= 0)
maxPasswordLen = passwordField.maxLength;
return fit;
}, this);
var logins = foundLogins.filter(function (l) {
var fit = (l.username.length <= maxUsernameLen &&
l.password.length <= maxPasswordLen);
if (!fit)
log("Ignored", l.username, "login: won't fit");
if (logins.length == 0) {
log("form not filled, none of the logins fit in the field");
recordAutofillResult(AUTOFILL_RESULT.NO_LOGINS_FIT);
return [false, foundLogins];
}
return fit;
}, this);
// The reason we didn't end up filling the form, if any. We include
// this in the formInfo object we send with the passwordmgr-found-logins
// notification. See the _notifyFoundLogins docs for possible values.
var didntFillReason = null;
if (logins.length == 0) {
log("form not filled, none of the logins fit in the field");
recordAutofillResult(AUTOFILL_RESULT.NO_LOGINS_FIT);
return;
}
// Attach autocomplete stuff to the username field, if we have
// one. This is normally used to select from multiple accounts,
// but even with one account we should refill if the user edits.
if (usernameField)
this._formFillService.markAsLoginManagerField(usernameField);
// Attach autocomplete stuff to the username field, if we have
// one. This is normally used to select from multiple accounts,
// but even with one account we should refill if the user edits.
if (usernameField)
this._formFillService.markAsLoginManagerField(usernameField);
// Don't clobber an existing password.
if (passwordField.value && !clobberPassword) {
didntFillReason = "existingPassword";
this._notifyFoundLogins(didntFillReason, usernameField,
passwordField, foundLogins, null);
log("form not filled, the password field was already filled");
recordAutofillResult(AUTOFILL_RESULT.EXISTING_PASSWORD);
return [false, foundLogins];
}
// Don't clobber an existing password.
if (passwordField.value && !clobberPassword) {
log("form not filled, the password field was already filled");
recordAutofillResult(AUTOFILL_RESULT.EXISTING_PASSWORD);
return;
}
// If the form has an autocomplete=off attribute in play, don't
// fill in the login automatically. We check this after attaching
// the autocomplete stuff to the username field, so the user can
// still manually select a login to be filled in.
var isFormDisabled = false;
if (!ignoreAutocomplete &&
(this._isAutocompleteDisabled(form) ||
this._isAutocompleteDisabled(usernameField) ||
this._isAutocompleteDisabled(passwordField))) {
// Select a login to use for filling in the form.
var selectedLogin;
if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) {
// If username was specified in the field, it's disabled or it's readOnly, only fill in the
// password if we find a matching login.
var username = usernameField.value.toLowerCase();
isFormDisabled = true;
log("form not filled, has autocomplete=off");
}
let matchingLogins = logins.filter(function(l)
l.username.toLowerCase() == username);
if (matchingLogins.length == 0) {
log("Password not filled. None of the stored logins match the username already present.");
recordAutofillResult(AUTOFILL_RESULT.EXISTING_USERNAME);
return;
}
// Variable such that we reduce code duplication and can be sure we
// should be firing notifications if and only if we can fill the form.
var selectedLogin = null;
if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) {
// If username was specified in the field, it's disabled or it's readOnly, only fill in the
// password if we find a matching login.
var username = usernameField.value.toLowerCase();
let matchingLogins = logins.filter(function(l)
l.username.toLowerCase() == username);
if (matchingLogins.length) {
// If there are multiple, and one matches case, use it
for (let l of matchingLogins) {
if (l.username == usernameField.value) {
@@ -720,32 +760,42 @@ var LoginManagerContent = {
if (!selectedLogin) {
selectedLogin = matchingLogins[0];
}
} else if (logins.length == 1) {
selectedLogin = logins[0];
} else {
didntFillReason = "existingUsername";
log("Password not filled. None of the stored logins match the username already present.");
}
} else if (logins.length == 1) {
selectedLogin = logins[0];
} else {
// We have multiple logins. Handle a special case here, for sites
// which have a normal user+pass login *and* a password-only login
// (eg, a PIN). Prefer the login that matches the type of the form
// (user+pass or pass-only) when there's exactly one that matches.
let matchingLogins;
if (usernameField)
matchingLogins = logins.filter(function(l) l.username);
else
matchingLogins = logins.filter(function(l) !l.username);
if (matchingLogins.length == 1) {
selectedLogin = matchingLogins[0];
} else {
didntFillReason = "multipleLogins";
log("Multiple logins for form, so not filling any.");
}
}
// We have multiple logins. Handle a special case here, for sites
// which have a normal user+pass login *and* a password-only login
// (eg, a PIN). Prefer the login that matches the type of the form
// (user+pass or pass-only) when there's exactly one that matches.
let matchingLogins;
if (usernameField)
matchingLogins = logins.filter(function(l) l.username);
else
matchingLogins = logins.filter(function(l) !l.username);
if (matchingLogins.length != 1) {
log("Multiple logins for form, so not filling any.");
recordAutofillResult(AUTOFILL_RESULT.MULTIPLE_LOGINS);
return;
}
selectedLogin = matchingLogins[0];
}
// We will always have a selectedLogin at this point.
if (!autofillForm) {
log("autofillForms=false but form can be filled");
recordAutofillResult(AUTOFILL_RESULT.NO_AUTOFILL_FORMS);
return;
}
if (isAutocompleteOff && !ignoreAutocomplete) {
log("Not filling the login because we're respecting autocomplete=off");
recordAutofillResult(AUTOFILL_RESULT.AUTOCOMPLETE_OFF);
return;
}
var didFillForm = false;
if (selectedLogin && autofillForm && !isFormDisabled) {
// Fill the form
if (usernameField) {
@@ -766,106 +816,15 @@ var LoginManagerContent = {
if (passwordField.value != selectedLogin.password) {
passwordField.setUserInput(selectedLogin.password);
}
didFillForm = true;
} else if (selectedLogin && !autofillForm) {
// For when autofillForm is false, but we still have the information
// to fill a form, we notify observers.
didntFillReason = "noAutofillForms";
Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
log("autofillForms=false but form can be filled; notified observers");
} else if (selectedLogin && isFormDisabled) {
// For when autocomplete is off, but we still have the information
// to fill a form, we notify observers.
didntFillReason = "autocompleteOff";
Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
log("autocomplete=off but form can be filled; notified observers");
}
this._notifyFoundLogins(didntFillReason, usernameField, passwordField,
foundLogins, selectedLogin);
if (didFillForm) {
recordAutofillResult(AUTOFILL_RESULT.FILLED);
let doc = form.ownerDocument;
let win = doc.defaultView;
let messageManager = messageManagerFromWindow(win);
messageManager.sendAsyncMessage("LoginStats:LoginFillSuccessful");
} else {
let autofillResult = AUTOFILL_RESULT.UNKNOWN_FAILURE;
switch (didntFillReason) {
// existingPassword is already handled above
case "existingUsername":
autofillResult = AUTOFILL_RESULT.EXISTING_USERNAME;
break;
case "multipleLogins":
autofillResult = AUTOFILL_RESULT.MULTIPLE_LOGINS;
break;
case "noAutofillForms":
autofillResult = AUTOFILL_RESULT.NO_AUTOFILL_FORMS;
break;
case "autocompleteOff":
autofillResult = AUTOFILL_RESULT.AUTOCOMPLETE_OFF;
break;
}
recordAutofillResult(autofillResult);
} finally {
Services.obs.notifyObservers(form, "passwordmgr-processed-form", null);
}
return [didFillForm, foundLogins];
},
/**
* Notify observers about an attempt to fill a form that resulted in some
* saved logins being found for the form.
*
* This does not get called if the login manager attempts to fill a form
* but does not find any saved logins. It does, however, get called when
* the login manager does find saved logins whether or not it actually
* fills the form with one of them.
*
* @param didntFillReason {String}
* the reason the login manager didn't fill the form, if any;
* if the value of this parameter is null, then the form was filled;
* otherwise, this parameter will be one of these values:
* existingUsername: the username field already contains a username
* that doesn't match any stored usernames
* existingPassword: the password field already contains a password
* autocompleteOff: autocomplete has been disabled for the form
* or its username or password fields
* multipleLogins: we have multiple logins for the form
* noAutofillForms: the autofillForms pref is set to false
*
* @param usernameField {HTMLInputElement}
* the username field detected by the login manager, if any;
* otherwise null
*
* @param passwordField {HTMLInputElement}
* the password field detected by the login manager
*
* @param foundLogins {Array}
* an array of nsILoginInfos that can be used to fill the form
*
* @param selectedLogin {nsILoginInfo}
* the nsILoginInfo that was/would be used to fill the form, if any;
* otherwise null; whether or not it was actually used depends on
* the value of the didntFillReason parameter
*/
_notifyFoundLogins : function (didntFillReason, usernameField,
passwordField, foundLogins, selectedLogin) {
// We need .setProperty(), which is a method on the original
// nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2
// doesn't inherit from that, so the additional QI is needed.
let formInfo = Cc["@mozilla.org/hash-property-bag;1"].
createInstance(Ci.nsIWritablePropertyBag2).
QueryInterface(Ci.nsIWritablePropertyBag);
formInfo.setPropertyAsACString("didntFillReason", didntFillReason);
formInfo.setPropertyAsInterface("usernameField", usernameField);
formInfo.setPropertyAsInterface("passwordField", passwordField);
formInfo.setProperty("foundLogins", foundLogins.concat());
formInfo.setPropertyAsInterface("selectedLogin", selectedLogin);
Services.obs.notifyObservers(formInfo, "passwordmgr-found-logins", null);
},
};
@@ -964,7 +923,7 @@ UserAutoCompleteResult.prototype = {
getValueAt : function (index) {
if (index < 0 || index >= this.logins.length)
throw "Index out of range.";
throw new Error("Index out of range.");
return this.logins[index].username;
},
@@ -991,7 +950,7 @@ UserAutoCompleteResult.prototype = {
removeValueAt : function (index, removeFromDB) {
if (index < 0 || index >= this.logins.length)
throw "Index out of range.";
throw new Error("Index out of range.");
var [removedLogin] = this.logins.splice(index, 1);
@@ -5,12 +5,12 @@
"use strict";
const Cu = Components.utils;
const Ci = Components.interfaces;
const Cc = Components.classes;
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UserAutoCompleteResult",
"resource://gre/modules/LoginManagerContent.jsm");
@@ -21,36 +21,6 @@ this.EXPORTED_SYMBOLS = [ "LoginManagerParent", "PasswordsMetricsProvider" ];
var gDebug;
function log(...pieces) {
function generateLogMessage(args) {
let strings = ['Login Manager (parent):'];
args.forEach(function(arg) {
if (typeof arg === 'string') {
strings.push(arg);
} else if (typeof arg === 'undefined') {
strings.push('undefined');
} else if (arg === null) {
strings.push('null');
} else {
try {
strings.push(JSON.stringify(arg, null, 2));
} catch(err) {
strings.push("<<something>>");
}
}
});
return strings.join(' ');
}
if (!gDebug)
return;
let message = generateLogMessage(pieces);
dump(message + "\n");
Services.console.logStringMessage(message);
}
#ifndef ANDROID
#ifdef MOZ_SERVICES_HEALTHREPORT
XPCOMUtils.defineLazyModuleGetter(this, "Metrics",
@@ -152,6 +122,37 @@ PasswordsMeasurement2.prototype = Object.freeze({
#endif
#endif
function log(...pieces) {
function generateLogMessage(args) {
let strings = ['Login Manager (parent):'];
args.forEach(function(arg) {
if (typeof arg === 'string') {
strings.push(arg);
} else if (typeof arg === 'undefined') {
strings.push('undefined');
} else if (arg === null) {
strings.push('null');
} else {
try {
strings.push(JSON.stringify(arg, null, 2));
} catch(err) {
strings.push("<<something>>");
}
}
});
return strings.join(' ');
}
if (!gDebug)
return;
let message = generateLogMessage(pieces);
dump(message + "\n");
Services.console.logStringMessage(message);
}
function prefChanged() {
gDebug = Services.prefs.getBoolPref("signon.debug");
}
@@ -169,6 +170,12 @@ var LoginManagerParent = {
mm.addMessageListener("LoginStats:LoginEncountered", this);
mm.addMessageListener("LoginStats:LoginFillSuccessful", this);
Services.obs.addObserver(this, "LoginStats:NewSavedPassword", false);
XPCOMUtils.defineLazyGetter(this, "recipeParentPromise", () => {
const { LoginRecipesParent } = Cu.import("resource://gre/modules/LoginRecipes.jsm", {});
let parent = new LoginRecipesParent();
return parent.initializationPromise;
});
},
observe: function (aSubject, aTopic, aData) {
@@ -187,11 +194,11 @@ var LoginManagerParent = {
switch (msg.name) {
case "RemoteLogins:findLogins": {
// TODO Verify msg.target's principals against the formOrigin?
this.findLogins(data.options.showMasterPassword,
data.formOrigin,
data.actionOrigin,
data.requestId,
msg.target.messageManager);
this.sendLoginDataToChild(data.options.showMasterPassword,
data.formOrigin,
data.actionOrigin,
data.requestId,
msg.target.messageManager);
break;
}
@@ -232,19 +239,35 @@ var LoginManagerParent = {
}
},
findLogins: function(showMasterPassword, formOrigin, actionOrigin,
requestId, target) {
/**
* Send relevant data (e.g. logins and recipes) to the child process (LoginManagerContent).
*/
sendLoginDataToChild: Task.async(function*(showMasterPassword, formOrigin, actionOrigin,
requestId, target) {
let recipes = [];
if (formOrigin) {
let formHost = (new URL(formOrigin)).host;
let recipeManager = yield this.recipeParentPromise;
recipes = recipeManager.getRecipesForHost(formHost);
}
if (!showMasterPassword && !Services.logins.isLoggedIn) {
target.sendAsyncMessage("RemoteLogins:loginsFound",
{ requestId: requestId, logins: [] });
target.sendAsyncMessage("RemoteLogins:loginsFound", {
requestId: requestId,
logins: [],
recipes,
});
return;
}
let allLoginsCount = Services.logins.countLogins(formOrigin, "", null);
// If there are no logins for this site, bail out now.
if (!allLoginsCount) {
target.sendAsyncMessage("RemoteLogins:loginsFound",
{ requestId: requestId, logins: [] });
target.sendAsyncMessage("RemoteLogins:loginsFound", {
requestId: requestId,
logins: [],
recipes,
});
return;
}
@@ -263,13 +286,16 @@ var LoginManagerParent = {
Services.obs.removeObserver(this, "passwordmgr-crypto-login");
Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
if (topic == "passwordmgr-crypto-loginCanceled") {
target.sendAsyncMessage("RemoteLogins:loginsFound",
{ requestId: requestId, logins: [] });
target.sendAsyncMessage("RemoteLogins:loginsFound", {
requestId: requestId,
logins: [],
recipes,
});
return;
}
self.findLogins(showMasterPassword, formOrigin, actionOrigin,
requestId, target);
self.sendLoginDataToChild(showMasterPassword, formOrigin, actionOrigin,
requestId, target);
},
};
@@ -284,8 +310,14 @@ var LoginManagerParent = {
}
var logins = Services.logins.findLogins({}, formOrigin, actionOrigin, null);
target.sendAsyncMessage("RemoteLogins:loginsFound",
{ requestId: requestId, logins: logins });
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
// doesn't support structured cloning.
var jsLogins = JSON.parse(JSON.stringify(logins));
target.sendAsyncMessage("RemoteLogins:loginsFound", {
requestId: requestId,
logins: jsLogins,
recipes,
});
const PWMGR_FORM_ACTION_EFFECT = Services.telemetry.getHistogramById("PWMGR_FORM_ACTION_EFFECT");
if (logins.length == 0) {
@@ -296,7 +328,7 @@ var LoginManagerParent = {
// logins.length < allLoginsCount
PWMGR_FORM_ACTION_EFFECT.add(1);
}
},
}),
doAutocompleteSearch: function({ formOrigin, actionOrigin,
searchString, previousResult,
@@ -339,9 +371,13 @@ var LoginManagerParent = {
AutoCompleteE10S.showPopupWithResults(target.ownerDocument.defaultView, rect, result);
}
target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted",
{ requestId: requestId,
logins: matchingLogins });
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
// doesn't support structured cloning.
var jsLogins = JSON.parse(JSON.stringify(matchingLogins));
target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted", {
requestId: requestId,
logins: jsLogins,
});
},
onFormSubmit: function(hostname, formSubmitURL,
@@ -0,0 +1,246 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["LoginRecipesContent", "LoginRecipesParent"];
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
const REQUIRED_KEYS = ["hosts"];
const OPTIONAL_KEYS = ["description", "passwordSelector", "pathRegex", "usernameSelector"];
const SUPPORTED_KEYS = REQUIRED_KEYS.concat(OPTIONAL_KEYS);
Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
"resource://gre/modules/LoginHelper.jsm");
XPCOMUtils.defineLazyGetter(this, "log", () => LoginHelper.createLogger("LoginRecipes"));
/**
* Create an instance of the object to manage recipes in the parent process.
* Consumers should wait until {@link initializationPromise} resolves before
* calling methods on the object.
*
* @constructor
* @param {boolean} [aOptions.defaults=true] whether to load default application recipes.
*/
function LoginRecipesParent(aOptions = { defaults: true }) {
if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
throw new Error("LoginRecipesParent should only be used from the main process");
}
this._recipesByHost = new Map();
if (aOptions.defaults) {
// XXX: Bug 1134850 will handle reading recipes from a file.
this.initializationPromise = this.load(DEFAULT_RECIPES).then(resolve => {
return this;
});
} else {
this.initializationPromise = Promise.resolve(this);
}
}
LoginRecipesParent.prototype = {
/**
* Promise resolved with an instance of itself when the module is ready.
*
* @type {Promise}
*/
initializationPromise: null,
/**
* @type {Map} Map of hosts (including non-default port numbers) to Sets of recipes.
* e.g. "example.com:8080" => Set({...})
*/
_recipesByHost: null,
/**
* @param {Object} aRecipes an object containing recipes to load for use. The object
* should be compatible with JSON (e.g. no RegExp).
* @return {Promise} resolving when the recipes are loaded
*/
load(aRecipes) {
let recipeErrors = 0;
for (let rawRecipe of aRecipes.siteRecipes) {
try {
rawRecipe.pathRegex = rawRecipe.pathRegex ? new RegExp(rawRecipe.pathRegex) : undefined;
this.add(rawRecipe);
} catch (ex) {
recipeErrors++;
log.error("Error loading recipe", rawRecipe, ex);
}
}
if (recipeErrors) {
return Promise.reject(`There were ${recipeErrors} recipe error(s)`);
}
return Promise.resolve();
},
/**
* Validate the recipe is sane and then add it to the set of recipes.
*
* @param {Object} recipe
*/
add(recipe) {
log.debug("Adding recipe:", recipe);
let recipeKeys = Object.keys(recipe);
let unknownKeys = recipeKeys.filter(key => SUPPORTED_KEYS.indexOf(key) == -1);
if (unknownKeys.length > 0) {
throw new Error("The following recipe keys aren't supported: " + unknownKeys.join(", "));
}
let missingRequiredKeys = REQUIRED_KEYS.filter(key => recipeKeys.indexOf(key) == -1);
if (missingRequiredKeys.length > 0) {
throw new Error("The following required recipe keys are missing: " + missingRequiredKeys.join(", "));
}
if (!Array.isArray(recipe.hosts)) {
throw new Error("'hosts' must be a array");
}
if (!recipe.hosts.length) {
throw new Error("'hosts' must be a non-empty array");
}
if (recipe.pathRegex && recipe.pathRegex.constructor.name != "RegExp") {
throw new Error("'pathRegex' must be a regular expression");
}
const OPTIONAL_STRING_PROPS = ["description", "passwordSelector", "usernameSelector"];
for (let prop of OPTIONAL_STRING_PROPS) {
if (recipe[prop] && typeof(recipe[prop]) != "string") {
throw new Error(`'${prop}' must be a string`);
}
}
// Add the recipe to the map for each host
for (let host of recipe.hosts) {
if (!this._recipesByHost.has(host)) {
this._recipesByHost.set(host, new Set());
}
this._recipesByHost.get(host).add(recipe);
}
},
/**
* Currently only exact host matches are returned but this will eventually handle parent domains.
*
* @param {String} aHost (e.g. example.com:8080 [non-default port] or sub.example.com)
* @return {Set} of recipes that apply to the host ordered by host priority
*/
getRecipesForHost(aHost) {
let hostRecipes = this._recipesByHost.get(aHost);
if (!hostRecipes) {
return new Set();
}
return hostRecipes;
},
};
let LoginRecipesContent = {
/**
* @param {Set} aRecipes - Possible recipes that could apply to the form
* @param {HTMLFormElement} aForm - We use a form instead of just a URL so we can later apply
* tests to the page contents.
* @return {Set} a subset of recipes that apply to the form with the order preserved
*/
_filterRecipesForForm(aRecipes, aForm) {
let formDocURL = aForm.ownerDocument.location;
let host = formDocURL.host;
let hostRecipes = aRecipes;
let recipes = new Set();
log.debug("_filterRecipesForForm", aRecipes);
if (!hostRecipes) {
return recipes;
}
for (let hostRecipe of hostRecipes) {
if (hostRecipe.pathRegex && !hostRecipe.pathRegex.test(formDocURL.pathname)) {
continue;
}
recipes.add(hostRecipe);
}
return recipes;
},
/**
* Given a set of recipes that apply to the host, choose the one most applicable for
* overriding login fields in the form.
*
* @param {Set} aRecipes The set of recipes to consider for the form
* @param {HTMLFormElement} aForm The form where login fields exist.
* @return {Object} The recipe that is most applicable for the form.
*/
getFieldOverrides(aRecipes, aForm) {
let recipes = this._filterRecipesForForm(aRecipes, aForm);
log.debug("getFieldOverrides: filtered recipes:", recipes);
if (!recipes.size) {
return null;
}
let chosenRecipe = null;
// Find the first (most-specific recipe that involves field overrides).
for (let recipe of recipes) {
if (!recipe.usernameSelector && !recipe.passwordSelector) {
continue;
}
chosenRecipe = recipe;
break;
}
return chosenRecipe;
},
/**
* @param {HTMLElement} aParent the element to query for the selector from.
* @param {CSSSelector} aSelector the CSS selector to query for the login field.
* @return {HTMLElement|null}
*/
queryLoginField(aParent, aSelector) {
if (!aSelector) {
return null;
}
let field = aParent.ownerDocument.querySelector(aSelector);
if (!field) {
log.warn("Login field selector wasn't matched:", aSelector);
return null;
}
if (!(field instanceof aParent.ownerDocument.defaultView.HTMLInputElement)) {
log.warn("Login field isn't an <input> so ignoring it:", aSelector);
return null;
}
return field;
},
};
const DEFAULT_RECIPES = {
"siteRecipes": [
{
"description": "okta uses a hidden password field to disable filling",
"hosts": ["mozilla.okta.com"],
"passwordSelector": "#pass-signin"
},
{
"description": "anthem uses a hidden password and username field to disable filling",
"hosts": ["www.anthem.com"],
"passwordSelector": "#LoginContent_txtLoginPass"
},
{
"description": "An ephemeral password-shim field is incorrectly selected as the username field.",
"hosts": ["www.discover.com"],
"usernameSelector": "#login-account"
}
]
};
+1
View File
@@ -39,6 +39,7 @@ EXTRA_JS_MODULES += [
'InsecurePasswordUtils.jsm',
'LoginHelper.jsm',
'LoginManagerContent.jsm',
'LoginRecipes.jsm',
'OSCrypto.jsm',
]
@@ -13,7 +13,7 @@ interface nsIDOMHTMLInputElement;
interface nsIDOMHTMLFormElement;
interface nsIPropertyBag;
[scriptable, uuid(f0c5ca21-db71-4b32-993e-ab63054cc6f5)]
[scriptable, uuid(38c7f6af-7df9-49c7-b558-2776b24e6cc1)]
interface nsILoginManager : nsISupports {
/**
* This promise is resolved when initialization is complete, and is rejected
@@ -215,16 +215,6 @@ interface nsILoginManager : nsISupports {
in nsIDOMHTMLInputElement aElement,
in nsIFormAutoCompleteObserver aListener);
/**
* Fill a form with login information if we have it. This method will fill
* aForm regardless of the signon.autofillForms preference.
*
* @param aForm
* The form to fill
* @return Promise that is resolved with whether or not the form was filled.
*/
jsval fillForm(in nsIDOMHTMLFormElement aForm);
/**
* Search for logins in the login manager. An array is always returned;
* if there are no logins the array is empty.
@@ -3,9 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
@@ -74,7 +72,7 @@ LoginManager.prototype = {
return this;
}
throw Cr.NS_ERROR_NO_INTERFACE;
throw new Components.Exception("Interface not available", Cr.NS_ERROR_NO_INTERFACE);
},
@@ -301,26 +299,26 @@ LoginManager.prototype = {
addLogin : function (login) {
// Sanity check the login
if (login.hostname == null || login.hostname.length == 0)
throw "Can't add a login with a null or empty hostname.";
throw new Error("Can't add a login with a null or empty hostname.");
// For logins w/o a username, set to "", not null.
if (login.username == null)
throw "Can't add a login with a null username.";
throw new Error("Can't add a login with a null username.");
if (login.password == null || login.password.length == 0)
throw "Can't add a login with a null or empty password.";
throw new Error("Can't add a login with a null or empty password.");
if (login.formSubmitURL || login.formSubmitURL == "") {
// We have a form submit URL. Can't have a HTTP realm.
if (login.httpRealm != null)
throw "Can't add a login with both a httpRealm and formSubmitURL.";
throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
} else if (login.httpRealm) {
// We have a HTTP realm. Can't have a form submit URL.
if (login.formSubmitURL != null)
throw "Can't add a login with both a httpRealm and formSubmitURL.";
throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
} else {
// Need one or the other!
throw "Can't add a login without a httpRealm or formSubmitURL.";
throw new Error("Can't add a login without a httpRealm or formSubmitURL.");
}
@@ -329,7 +327,7 @@ LoginManager.prototype = {
login.httpRealm);
if (logins.some(function(l) login.matches(l, true)))
throw "This login already exists.";
throw new Error("This login already exists.");
log("Adding login");
return this._storage.addLogin(login);
@@ -480,7 +478,7 @@ LoginManager.prototype = {
setLoginSavingEnabled : function (hostname, enabled) {
// Nulls won't round-trip with getAllDisabledHosts().
if (hostname.indexOf("\0") != -1)
throw "Invalid hostname";
throw new Error("Invalid hostname");
log("Login saving for", hostname, "now enabled?", enabled);
return this._storage.setLoginSavingEnabled(hostname, enabled);
@@ -3,15 +3,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Components.utils.import("resource://gre/modules/SharedPromptUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/SharedPromptUtils.jsm");
/* Constants for password prompt telemetry.
* Mirrored in mobile/android/components/LoginManagerPrompter.js */
@@ -136,7 +133,7 @@ LoginManagerPromptFactory.prototype = {
}
self._doAsyncPrompt();
}
}
};
Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
this.log("_doAsyncPrompt:run dispatched");
@@ -241,7 +238,7 @@ LoginManagerPrompter.prototype = {
this.__strBundle = bunService.createBundle(
"chrome://passwordmgr/locale/passwordmgr.properties");
if (!this.__strBundle)
throw "String bundle for Login Manager not present!";
throw new Error("String bundle for Login Manager not present!");
}
return this.__strBundle;
@@ -304,7 +301,8 @@ LoginManagerPrompter.prototype = {
prompt : function (aDialogTitle, aText, aPasswordRealm,
aSavePassword, aDefaultText, aResult) {
if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER)
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
throw new Components.Exception("prompt only supports SAVE_PASSWORD_NEVER",
Cr.NS_ERROR_NOT_IMPLEMENTED);
this.log("===== prompt() called =====");
@@ -328,7 +326,8 @@ LoginManagerPrompter.prototype = {
this.log("===== promptUsernameAndPassword() called =====");
if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
throw new Components.Exception("promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
Cr.NS_ERROR_NOT_IMPLEMENTED);
var selectedLogin = null;
var checkBox = { value : false };
@@ -430,7 +429,8 @@ LoginManagerPrompter.prototype = {
this.log("===== promptPassword called() =====");
if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
throw new Components.Exception("promptPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
Cr.NS_ERROR_NOT_IMPLEMENTED);
var checkBox = { value : false };
var checkBoxLabel = null;
@@ -689,7 +689,7 @@ LoginManagerPrompter.prototype = {
level: aLevel,
inProgress : false,
prompter: this
}
};
this._factory._asyncPrompts[hashKey] = asyncPrompt;
this._factory._doAsyncPrompt();
@@ -1451,11 +1451,11 @@ LoginManagerPrompter.prototype = {
if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
this.log("getAuthTarget is for proxy auth");
if (!(aChannel instanceof Ci.nsIProxiedChannel))
throw "proxy auth needs nsIProxiedChannel";
throw new Error("proxy auth needs nsIProxiedChannel");
var info = aChannel.proxyInfo;
if (!info)
throw "proxy auth needs nsIProxyInfo";
throw new Error("proxy auth needs nsIProxyInfo");
// Proxies don't have a scheme, but we'll use "moz-proxy://"
// so that it's more obvious what the login is for.
@@ -1538,7 +1538,7 @@ LoginManagerPrompter.prototype = {
this.callback = null;
this.context = null;
}
}
};
}
}; // end of LoginManagerPrompter implementation
@@ -115,7 +115,7 @@ this.LoginManagerStorage_json.prototype = {
}.bind(this)).catch(Cu.reportError);
} catch (e) {
this.log("Initialization failed: " + e);
throw "Initialization failed";
throw new Error("Initialization failed");
}
},
@@ -151,7 +151,7 @@ this.LoginManagerStorage_json.prototype = {
loginClone.QueryInterface(Ci.nsILoginMetaInfo);
if (loginClone.guid) {
if (!this._isGuidUnique(loginClone.guid))
throw "specified GUID already exists";
throw new Error("specified GUID already exists");
} else {
loginClone.guid = gUUIDGenerator.generateUUID().toString();
}
@@ -199,7 +199,7 @@ this.LoginManagerStorage_json.prototype = {
let [idToDelete, storedLogin] = this._getIdForLogin(login);
if (!idToDelete)
throw "No matching logins";
throw new Error("No matching logins");
let foundIndex = this._store.data.logins.findIndex(l => l.id == idToDelete);
if (foundIndex != -1) {
@@ -220,7 +220,7 @@ this.LoginManagerStorage_json.prototype = {
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
if (!idToModify)
throw "No matching logins";
throw new Error("No matching logins");
let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
@@ -228,7 +228,7 @@ this.LoginManagerStorage_json.prototype = {
if (newLogin.guid != oldStoredLogin.guid &&
!this._isGuidUnique(newLogin.guid))
{
throw "specified GUID already exists";
throw new Error("specified GUID already exists");
}
// Look for an existing entry in case key properties changed.
@@ -238,7 +238,7 @@ this.LoginManagerStorage_json.prototype = {
newLogin.httpRealm);
if (logins.some(login => newLogin.matches(login, true)))
throw "This login already exists.";
throw new Error("This login already exists.");
}
// Get the encrypted value of the username and password.
@@ -362,7 +362,7 @@ this.LoginManagerStorage_json.prototype = {
break;
// Fail if caller requests an unknown property.
default:
throw "Unexpected field: " + field;
throw new Error("Unexpected field: " + field);
}
}
return true;
@@ -66,7 +66,7 @@ LoginManagerStorage_mozStorage.prototype = {
return this._dbConnection;
}
throw Cr.NS_ERROR_NO_INTERFACE;
throw new Components.Exception("Interface not available", Cr.NS_ERROR_NO_INTERFACE);
},
__crypto : null, // nsILoginManagerCrypto service
@@ -220,7 +220,7 @@ LoginManagerStorage_mozStorage.prototype = {
// If the import fails on first run, we want to delete the db
if (isFirstRun && e == "Import failed")
this._dbCleanup(false);
throw "Initialization failed";
throw new Error("Initialization failed");
}
},
@@ -253,7 +253,7 @@ LoginManagerStorage_mozStorage.prototype = {
loginClone.QueryInterface(Ci.nsILoginMetaInfo);
if (loginClone.guid) {
if (!this._isGuidUnique(loginClone.guid))
throw "specified GUID already exists";
throw new Error("specified GUID already exists");
} else {
loginClone.guid = this._uuidService.generateUUID().toString();
}
@@ -302,7 +302,7 @@ LoginManagerStorage_mozStorage.prototype = {
stmt.execute();
} catch (e) {
this.log("addLogin failed: " + e.name + " : " + e.message);
throw "Couldn't write to database, login not added.";
throw new Error("Couldn't write to database, login not added.");
} finally {
if (stmt) {
stmt.reset();
@@ -321,7 +321,7 @@ LoginManagerStorage_mozStorage.prototype = {
removeLogin : function (login) {
let [idToDelete, storedLogin] = this._getIdForLogin(login);
if (!idToDelete)
throw "No matching logins";
throw new Error("No matching logins");
// Execute the statement & remove from DB
let query = "DELETE FROM moz_logins WHERE id = :id";
@@ -335,7 +335,7 @@ LoginManagerStorage_mozStorage.prototype = {
transaction.commit();
} catch (e) {
this.log("_removeLogin failed: " + e.name + " : " + e.message);
throw "Couldn't write to database, login not removed.";
throw new Error("Couldn't write to database, login not removed.");
transaction.rollback();
} finally {
if (stmt) {
@@ -353,7 +353,7 @@ LoginManagerStorage_mozStorage.prototype = {
modifyLogin : function (oldLogin, newLoginData) {
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
if (!idToModify)
throw "No matching logins";
throw new Error("No matching logins");
let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
@@ -361,7 +361,7 @@ LoginManagerStorage_mozStorage.prototype = {
if (newLogin.guid != oldStoredLogin.guid &&
!this._isGuidUnique(newLogin.guid))
{
throw "specified GUID already exists";
throw new Error("specified GUID already exists");
}
// Look for an existing entry in case key properties changed.
@@ -371,7 +371,7 @@ LoginManagerStorage_mozStorage.prototype = {
newLogin.httpRealm);
if (logins.some(login => newLogin.matches(login, true)))
throw "This login already exists.";
throw new Error("This login already exists.");
}
// Get the encrypted value of the username and password.
@@ -417,7 +417,7 @@ LoginManagerStorage_mozStorage.prototype = {
stmt.execute();
} catch (e) {
this.log("modifyLogin failed: " + e.name + " : " + e.message);
throw "Couldn't write to database, login not modified.";
throw new Error("Couldn't write to database, login not modified.");
} finally {
if (stmt) {
stmt.reset();
@@ -519,7 +519,7 @@ LoginManagerStorage_mozStorage.prototype = {
break;
// Fail if caller requests an unknown property.
default:
throw "Unexpected field: " + field;
throw new Error("Unexpected field: " + field);
}
}
@@ -610,7 +610,7 @@ LoginManagerStorage_mozStorage.prototype = {
} catch (e) {
this.log("_removeAllLogins failed: " + e.name + " : " + e.message);
transaction.rollback();
throw "Couldn't write to database";
throw new Error("Couldn't write to database");
} finally {
if (stmt) {
stmt.reset();
@@ -669,7 +669,7 @@ LoginManagerStorage_mozStorage.prototype = {
stmt.execute();
} catch (e) {
this.log("setLoginSavingEnabled failed: " + e.name + " : " + e.message);
throw "Couldn't write to database"
throw new Error("Couldn't write to database");
} finally {
if (stmt) {
stmt.reset();
@@ -86,12 +86,12 @@ function reallyHandleRequest(request, response) {
authHeader = request.getHeader("Authorization");
match = /Basic (.+)/.exec(authHeader);
if (match.length != 2)
throw "Couldn't parse auth header: " + authHeader;
throw new Error("Couldn't parse auth header: " + authHeader);
var userpass = base64ToString(match[1]); // no atob() :-(
match = /(.*):(.*)/.exec(userpass);
if (match.length != 3)
throw "Couldn't decode auth header: " + userpass;
throw new Error("Couldn't decode auth header: " + userpass);
actual_user = match[1];
actual_pass = match[2];
}
@@ -101,12 +101,12 @@ function reallyHandleRequest(request, response) {
authHeader = request.getHeader("Proxy-Authorization");
match = /Basic (.+)/.exec(authHeader);
if (match.length != 2)
throw "Couldn't parse auth header: " + authHeader;
throw new Error("Couldn't parse auth header: " + authHeader);
var userpass = base64ToString(match[1]); // no atob() :-(
match = /(.*):(.*)/.exec(userpass);
if (match.length != 3)
throw "Couldn't decode auth header: " + userpass;
throw new Error("Couldn't decode auth header: " + userpass);
proxy_actual_user = match[1];
proxy_actual_pass = match[2];
}
@@ -91,12 +91,12 @@ function reallyHandleRequest(request, response) {
authHeader = request.getHeader("Authorization");
match = /Basic (.+)/.exec(authHeader);
if (match.length != 2)
throw "Couldn't parse auth header: " + authHeader;
throw new Error("Couldn't parse auth header: " + authHeader);
var userpass = base64ToString(match[1]); // no atob() :-(
match = /(.*):(.*)/.exec(userpass);
if (match.length != 3)
throw "Couldn't decode auth header: " + userpass;
throw new Error("Couldn't decode auth header: " + userpass);
actual_user = match[1];
actual_pass = match[2];
}
@@ -106,12 +106,12 @@ function reallyHandleRequest(request, response) {
authHeader = request.getHeader("Proxy-Authorization");
match = /Basic (.+)/.exec(authHeader);
if (match.length != 2)
throw "Couldn't parse auth header: " + authHeader;
throw new Error("Couldn't parse auth header: " + authHeader);
var userpass = base64ToString(match[1]); // no atob() :-(
match = /(.*):(.*)/.exec(userpass);
if (match.length != 3)
throw "Couldn't decode auth header: " + userpass;
throw new Error("Couldn't decode auth header: " + userpass);
proxy_actual_user = match[1];
proxy_actual_pass = match[2];
}
@@ -24,12 +24,12 @@ function handleRequest(request, response)
authHeader = request.getHeader("Authorization");
match = /Basic (.+)/.exec(authHeader);
if (match.length != 2)
throw "Couldn't parse auth header: " + authHeader;
throw new Error("Couldn't parse auth header: " + authHeader);
var userpass = base64ToString(match[1]); // no atob() :-(
match = /(.*):(.*)/.exec(userpass);
if (match.length != 3)
throw "Couldn't decode auth header: " + userpass;
throw new Error("Couldn't decode auth header: " + userpass);
actual_user = match[1];
actual_pass = match[2];
}
@@ -31,7 +31,6 @@ support-files =
[test_basic_form_0pw.html]
[test_basic_form_1pw.html]
[test_basic_form_1pw_2.html]
[test_basic_form_2.html]
[test_basic_form_2pw_1.html]
[test_basic_form_2pw_2.html]
[test_basic_form_3pw_1.html]
@@ -40,8 +39,6 @@ skip-if = toolkit == 'android'
[test_case_differences.html]
skip-if = toolkit == 'android'
[test_basic_form_html5.html]
[test_basic_form_observer_autofillForms.html]
[test_basic_form_observer_foundLogins.html]
[test_basic_form_pwevent.html]
[test_basic_form_pwonly.html]
[test_bug_221634.html]
@@ -72,6 +69,8 @@ skip-if = os == "linux" || toolkit == 'android' # bug 934057
skip-if = os == "linux" || toolkit == 'android' #TIMED_OUT
[test_prompt_async.html]
skip-if = toolkit == 'android' #TIMED_OUT
[test_recipe_login_fields.html]
skip-if = buildapp == 'mulet' || buildapp == 'b2g'
[test_xhr.html]
skip-if = toolkit == 'android' #TIMED_OUT
[test_xml_load.html]
@@ -182,18 +182,17 @@ function commonInit(selfFilling) {
form.appendChild(password);
var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
var bag = subject.QueryInterface(SpecialPowers.Ci.nsIPropertyBag2);
var username = bag.get("usernameField");
if (!username || username.form.id !== 'observerforcer')
var form = subject.QueryInterface(SpecialPowers.Ci.nsIDOMNode);
if (form.id !== 'observerforcer')
return;
SpecialPowers.removeObserver(observer, "passwordmgr-found-logins");
SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
form.parentNode.removeChild(form);
SimpleTest.executeSoon(() => {
var event = new Event("runTests");
window.dispatchEvent(event);
});
});
SpecialPowers.addObserver(observer, "passwordmgr-found-logins", false);
SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
document.body.appendChild(form);
});
@@ -259,3 +258,25 @@ function dumpLogin(label, login) {
loginText += login.passwordField;
ok(true, label + loginText);
}
// Code to run when loaded as a chrome script in tests via loadChromeScript
if (this.addMessageListener) {
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
var SpecialPowers = { Cc, Ci, Cr, Cu, };
var ok, is;
// Ignore ok/is in commonInit since they aren't defined in a chrome script.
ok = is = () => {};
Cu.import("resource://gre/modules/Task.jsm");
addMessageListener("setupParent", () => {
commonInit(true);
sendAsyncMessage("doneSetup");
});
addMessageListener("loadRecipes", Task.async(function* loadRecipes(recipes) {
var { LoginManagerParent } = Cu.import("resource://gre/modules/LoginManagerParent.jsm", {});
var recipeParent = yield LoginManagerParent.recipeParentPromise;
yield recipeParent.load(recipes);
sendAsyncMessage("loadedRecipes", recipes);
}));
}
@@ -1,64 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for Login Manager</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="pwmgr_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
Login Manager test: simple form fill with autofillForms disabled
<script>
commonInit();
SimpleTest.waitForExplicitFinish();
var pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"]
.getService(SpecialPowers.Ci.nsILoginManager);
</script>
<p id="display"></p>
<div id="content" style="display: block">
<form id="form1" action="formtest.js">
<p>This is form 1.</p>
<input type="text" name="uname">
<input type="password" name="pword">
<button type="submit">Submit</button>
<button type="reset"> Reset </button>
</form>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
/** Test for Login Manager: simple form fill with autofillForms disabled **/
function startTest(){
// Ensure the form is empty at start
is($_(1, "uname").value, "", "Checking for blank username");
is($_(1, "pword").value, "", "Checking for blank password");
// Call the public method, check return value
pwmgr.fillForm(document.getElementById("form1"))
.then(function(result) {
is(result, true, "Checking return value of fillForm");
// Check that the form was filled
is($_(1, "uname").value, "testuser", "Checking for filled username");
is($_(1, "pword").value, "testpass", "Checking for filled password");
SimpleTest.finish();
});
}
// Assume that the pref starts out true, so set to false
SpecialPowers.pushPrefEnv({"set":[["signon.autofillForms", false]]}, setup);
function setup() {
window.addEventListener("runTests", startTest);
}
</script>
</pre>
</body>
</html>
@@ -254,10 +254,10 @@ function* runTest() {
function waitForCompletion() {
var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
SpecialPowers.removeObserver(observer, "passwordmgr-found-logins");
SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
tester.next();
});
SpecialPowers.addObserver(observer, "passwordmgr-found-logins", false);
SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
}
/* test 1 */
@@ -1,104 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for Login Manager</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="pwmgr_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
Login Manager test: simple form with autofillForms disabled and notifying observers
<script>
commonInit(true);
SimpleTest.waitForExplicitFinish();
const Cc = SpecialPowers.Cc;
const Ci = SpecialPowers.Ci;
var TestObserver;
// Assume that the pref starts out true, so set to false
SpecialPowers.pushPrefEnv({"set":[["signon.autofillForms", false]]}, function() {
TestObserver = {
receivedNotificationFoundForm : false,
receivedNotificationFoundLogins : false,
dataFoundForm : "",
dataFoundLogins : null,
observe : function (subject, topic, data) {
var pwmgr = Cc["@mozilla.org/login-manager;1"].
getService(Ci.nsILoginManager);
if (topic == "passwordmgr-found-form") {
info("got passwordmgr-found-form");
this.receivedNotificationFoundForm = true;
this.dataFoundForm = data;
// Now fill the form
pwmgr.fillForm(subject)
.then(window.startTest)
.then(null, function(e) { alert(e); });
} else if (topic == "passwordmgr-found-logins") {
info("got passwordmgr-found-logins");
// We only care about the first notification (the second comes from our
// own call to pwmgr.fillForm.
if (this.receivedNotificationFoundLogins)
return;
this.receivedNotificationFoundLogins = true;
this.dataFoundLogins = subject.QueryInterface(Ci.nsIPropertyBag2);
}
}
};
// Add the observer
SpecialPowers.addObserver(TestObserver, "passwordmgr-found-form", false);
SpecialPowers.addObserver(TestObserver, "passwordmgr-found-logins", false);
});
</script>
<p id="display"></p>
<div id="content" style="display: block">
<form id="form1" action="formtest.js">
<p>This is form 1.</p>
<input type="text" name="uname">
<input type="password" name="pword">
<button type="submit">Submit</button>
<button type="reset"> Reset </button>
</form>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
/** Test for Login Manager: simple form with autofillForms disabled and notifying observers **/
function startTest() {
// Test that found-form observer is notified & got correct data
is(TestObserver.receivedNotificationFoundForm, true, "Checking found-form observer was notified");
is(TestObserver.dataFoundForm, "noAutofillForms", "Checking found-form observer got correct data");
// Check that form1 was filled
is($_(1, "uname").value, "testuser", "Checking for filled username");
is($_(1, "pword").value, "testpass", "Checking for filled password");
// Test that found-logins observer is notified & got correct data
is(TestObserver.receivedNotificationFoundLogins, true, "Checking found-logins observer was notified");
is(TestObserver.dataFoundLogins.get("didntFillReason"), "noAutofillForms", "Checking didntFillReason is noAutofillForms");
is(SpecialPowers.unwrap(TestObserver.dataFoundLogins.get("usernameField")), $_(1, "uname"), "Checking username field is correct");
is(SpecialPowers.unwrap(TestObserver.dataFoundLogins.get("passwordField")), $_(1, "pword"), "Checking password field is correct");
is(TestObserver.dataFoundLogins.get("foundLogins").constructor.name, "Array", "Checking foundLogins is array");
is(TestObserver.dataFoundLogins.get("foundLogins").length, 1, "Checking foundLogins contains one login");
ok(TestObserver.dataFoundLogins.get("selectedLogin").QueryInterface(Ci.nsILoginInfo), "Checking selectedLogin is nsILoginInfo");
ok(TestObserver.dataFoundLogins.get("selectedLogin").equals(TestObserver.dataFoundLogins.get("foundLogins")[0]),
"Checking selectedLogin is found login");
// Remove the observer
SpecialPowers.removeObserver(TestObserver, "passwordmgr-found-form");
SpecialPowers.removeObserver(TestObserver, "passwordmgr-found-logins");
SimpleTest.finish();
}
</script>
</pre>
</body>
</html>
@@ -1,161 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for Login Manager</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="pwmgr_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
Login Manager test: notifying observers of passwordmgr-found-logins
<script>
commonInit();
SimpleTest.waitForExplicitFinish();
const Cc = SpecialPowers.Cc;
const Ci = SpecialPowers.Ci;
// Configure the login manager with two logins for one of the forms
// so we can do a multiple logins test.
var nsLoginInfo =
new SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
SpecialPowers.Ci.nsILoginInfo);
var login1 = new nsLoginInfo();
login1.init("http://mochi.test:8888", "http://www.example.com", null,
"testuser1", "testpass1", "uname", "pword");
var login2 = new nsLoginInfo();
login2.init("http://mochi.test:8888", "http://www.example.com", null,
"testuser2", "testpass2", "uname", "pword");
var pwmgr = Cc["@mozilla.org/login-manager;1"].
getService(Ci.nsILoginManager);
pwmgr.addLogin(login1);
pwmgr.addLogin(login2);
var TestObserver = {
results: {},
observe : function (subject, topic, data) {
if (topic == "passwordmgr-found-logins") {
var formInfo = subject.QueryInterface(Ci.nsIPropertyBag2);
var id = formInfo.get("passwordField").form.id;
this.results[id].receivedNotification = true;
this.results[id].data = formInfo;
}
}
};
// Initialize the object that stores the results of notifications.
for (var formID of ["form1", "form2", "form3", "form5"])
TestObserver.results[formID] = { receivedNotification: false, data: null };
// Add the observer
SpecialPowers.addObserver(TestObserver, "passwordmgr-found-logins", false);
</script>
<p id="display"></p>
<div id="content" style="display: block">
<form id="form1" action="formtest.js">
<p>This is form 1.</p>
<input type="text" name="uname">
<input type="password" name="pword">
<button type="submit">Submit</button>
<button type="reset"> Reset </button>
</form>
<form id="form2" action="formtest.js">
<p>This is form 2.</p>
<input type="text" name="uname" value="existing">
<input type="password" name="pword">
<button type="submit">Submit</button>
<button type="reset"> Reset </button>
</form>
<form id="form3" action="formtest.js">
<p>This is form 3.</p>
<input type="text" name="uname">
<input type="password" name="pword" value="existing">
<button type="submit">Submit</button>
<button type="reset"> Reset </button>
</form>
<form id="form5" action="http://www.example.com">
<p>This is form 5.</p>
<input type="text" name="uname">
<input type="password" name="pword">
<button type="submit">Submit</button>
<button type="reset"> Reset </button>
</form>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
/** Test for Login Manager: notifying observers of passwordmgr-found-logins **/
function startTest(){
// Test notification of a form that was filled.
is(TestObserver.results["form1"].receivedNotification, true, "Checking observer was notified");
is(TestObserver.results["form1"].data.get("didntFillReason"), null, "Checking didntFillReason is null");
is(SpecialPowers.unwrap(TestObserver.results["form1"].data.get("usernameField")), $_(1, "uname"), "Checking username field is correct");
is(SpecialPowers.unwrap(TestObserver.results["form1"].data.get("passwordField")), $_(1, "pword"), "Checking password field is correct");
is(TestObserver.results["form1"].data.get("foundLogins").constructor.name, "Array", "Checking foundLogins is array");
is(TestObserver.results["form1"].data.get("foundLogins").length, 1, "Checking foundLogins contains one login");
ok(TestObserver.results["form1"].data.get("selectedLogin").QueryInterface(Ci.nsILoginInfo), "Checking selectedLogin is nsILoginInfo");
ok(TestObserver.results["form1"].data.get("selectedLogin").equals(TestObserver.results["form1"].data.get("foundLogins")[0]),
"Checking selectedLogin is found login");
// Test notification of a form that wasn't filled because its username field
// already contained a value.
is(TestObserver.results["form2"].receivedNotification, true, "Checking observer was notified");
is(TestObserver.results["form2"].data.get("didntFillReason"), "existingUsername", "Checking didntFillReason is existingUsername");
is(SpecialPowers.unwrap(TestObserver.results["form2"].data.get("usernameField")), $_(2, "uname"), "Checking username field is correct");
is(SpecialPowers.unwrap(TestObserver.results["form2"].data.get("passwordField")), $_(2, "pword"), "Checking password field is correct");
is(TestObserver.results["form2"].data.get("foundLogins").constructor.name, "Array", "Checking foundLogins is array");
is(TestObserver.results["form2"].data.get("foundLogins").length, 1, "Checking foundLogins contains one login");
is(TestObserver.results["form2"].data.get("selectedLogin"), null, "Checking selectedLogin is null");
// Test notification of a form that wasn't filled because its password field
// already contained a value.
is(TestObserver.results["form3"].receivedNotification, true, "Checking observer was notified");
is(TestObserver.results["form3"].data.get("didntFillReason"), "existingPassword", "Checking didntFillReason is existingPassword");
is(SpecialPowers.unwrap(TestObserver.results["form3"].data.get("usernameField")), $_(3, "uname"), "Checking username field is correct");
is(SpecialPowers.unwrap(TestObserver.results["form3"].data.get("passwordField")), $_(3, "pword"), "Checking password field is correct");
is(TestObserver.results["form3"].data.get("foundLogins").constructor.name, "Array", "Checking foundLogins is array");
is(TestObserver.results["form3"].data.get("foundLogins").length, 1, "Checking foundLogins contains one login");
is(TestObserver.results["form3"].data.get("selectedLogin"), null, "Checking selectedLogin is null");
// Test notification of a form that wasn't filled because multiple logins
// are available for the form.
is(TestObserver.results["form5"].receivedNotification, true, "Checking observer was notified");
is(TestObserver.results["form5"].data.get("didntFillReason"), "multipleLogins", "Checking didntFillReason is multipleLogins");
is(SpecialPowers.unwrap(TestObserver.results["form5"].data.get("usernameField")), $_(5, "uname"), "Checking username field is correct");
is(SpecialPowers.unwrap(TestObserver.results["form5"].data.get("passwordField")), $_(5, "pword"), "Checking password field is correct");
is(TestObserver.results["form5"].data.get("foundLogins").constructor.name, "Array", "Checking foundLogins is array");
is(TestObserver.results["form5"].data.get("foundLogins").length, 2, "Checking foundLogins contains two logins");
is(TestObserver.results["form5"].data.get("selectedLogin"), null, "Checking selectedLogin is null");
// Test notification of a form that wasn't filled because autofillForms
// was set to false (didntFillReason == noAutofillForms) is done in
// test_basic_form_observer_autofillForms.html.
// Remove the observer
SpecialPowers.removeObserver(TestObserver, "passwordmgr-found-logins");
// Remove the logins added for the multiple logins test.
pwmgr.removeLogin(login1);
pwmgr.removeLogin(login2);
SimpleTest.finish();
}
window.addEventListener("runTests", startTest);
</script>
</pre>
</body>
</html>
@@ -120,10 +120,10 @@ function* runTest() {
function waitForCompletion() {
var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
SpecialPowers.removeObserver(observer, "passwordmgr-found-logins");
SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
tester.next();
});
SpecialPowers.addObserver(observer, "passwordmgr-found-logins", false);
SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
}
/* test 1 */
@@ -100,7 +100,7 @@ function loadNextTest() {
observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
SimpleTest.executeSoon(() => { iframe.contentWindow.postMessage("go", "*"); });
});
SpecialPowers.addObserver(observer, "passwordmgr-found-logins", false);
SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
}
ok(true, "Starting test #" + testNum);
@@ -255,7 +255,7 @@ function handleLoad(aEvent) {
aWin.close();
});
SpecialPowers.removeObserver(observer, "passwordmgr-found-logins");
SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
SimpleTest.finish();
}
}
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<title>Test for recipes overriding login fields</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script type="application/javascript;version=1.8">
SimpleTest.waitForExplicitFinish();
let parentScriptURL = SimpleTest.getTestFileURL("pwmgr_common.js");
mm = SpecialPowers.loadChromeScript(parentScriptURL);
// Tell the parent to setup test logins.
mm.sendAsyncMessage("setupParent");
// When the setup is done, load a recipe for this test.
mm.addMessageListener("doneSetup", function doneSetup() {
mm.sendAsyncMessage("loadRecipes", {
siteRecipes: [{
hosts: ["mochi.test:8888"],
usernameSelector: "input[name='uname1']",
passwordSelector: "input[name='pword2']",
}],
});
});
mm.addMessageListener("loadedRecipes", function loadedRecipes() {
// Append the form dynamically so autofill is triggered after setup above.
document.getElementById("content").innerHTML += `
<!-- form with recipe for the username and password -->
<form id="form1">
<input type="text" name="uname1" oninput="reportFill(this, true)">
<input type="text" name="uname2" oninput="reportFill(this, false)">
<input type="password" name="pword1" oninput="reportFill(this, false)">
<input type="password" name="pword2" oninput="reportFill(this, true)">
</form>`;
});
const EXPECTED_FILLS = 4;
let fillCount = 0;
function reportFill(element, expected) {
ok(expected, `${element.name} was filled`);
if (++fillCount == EXPECTED_FILLS) {
SimpleTest.finish();
} else if (fillCount == 2) {
document.getElementById("content").innerHTML = `
<!-- Fallback to the default heuristics since the selectors don't match -->
<form id="form2">
<input type="text" name="uname3" oninput="reportFill(this, false)">
<input type="text" name="uname4" oninput="reportFill(this, true)">
<input type="password" name="pword3" oninput="reportFill(this, true)">
<input type="password" name="pword4" oninput="reportFill(this, false)">
</form>`;
} else if (fillCount > EXPECTED_FILLS) {
ok(false, "Too many fills");
}
}
</script>
<p id="display"></p>
<div id="content">
// Forms are inserted dynamically
</div>
<pre id="test"></pre>
</body>
</html>
@@ -16,6 +16,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/LoginRecipes.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
"resource://gre/modules/DownloadPaths.jsm");
@@ -198,6 +199,45 @@ const LoginTest = {
}
};
const RecipeHelpers = {
initNewParent() {
return (new LoginRecipesParent({ defaults: false })).initializationPromise;
},
/**
* Create a document for the given URL containing the given HTML containing a
* form and return the <form>.
*/
createTestForm(aDocumentURL, aHTML = "<form>") {
let parser = Cc["@mozilla.org/xmlextras/domparser;1"].
createInstance(Ci.nsIDOMParser);
parser.init();
let parsedDoc = parser.parseFromString(aHTML, "text/html");
// Mock the document.location object so we can unit test without a frame. We use a proxy
// instead of just assigning to the property since it's not configurable or writable.
let document = new Proxy(parsedDoc, {
get(target, property, receiver) {
// document.location is normally null when a document is outside of a "browsing context".
// See https://html.spec.whatwg.org/#the-location-interface
if (property == "location") {
return new URL(aDocumentURL);
}
return target[property];
},
});
let form = parsedDoc.forms[0];
// Assign form.ownerDocument to the proxy so document.location works.
Object.defineProperty(form, "ownerDocument", {
value: document,
});
return form;
}
};
////////////////////////////////////////////////////////////////////////////////
//// Predefined test data
@@ -166,7 +166,7 @@ Services.obs.removeObserver(TestObserver, "passwordmgr-storage-changed");
LoginTest.clearData();
} catch (e) {
throw "FAILED in test #" + testnum + " -- " + testdesc + ": " + e;
throw new Error("FAILED in test #" + testnum + " -- " + testdesc + ": " + e);
}
});
@@ -0,0 +1,177 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests adding and retrieving LoginRecipes in the parent process.
*/
"use strict";
add_task(function* test_init() {
let parent = new LoginRecipesParent({ defaults: false });
let initPromise1 = parent.initializationPromise;
let initPromise2 = parent.initializationPromise;
Assert.strictEqual(initPromise1, initPromise2, "Check that the same promise is returned");
let recipesParent = yield initPromise1;
Assert.ok(recipesParent instanceof LoginRecipesParent, "Check init return value");
Assert.strictEqual(recipesParent._recipesByHost.size, 0, "Initially 0 recipes");
});
add_task(function* test_get_missing_host() {
let recipesParent = yield RecipeHelpers.initNewParent();
let exampleRecipes = recipesParent.getRecipesForHost("example.invalid");
Assert.strictEqual(exampleRecipes.size, 0, "Check recipe count for example.invalid");
});
add_task(function* test_add_get_simple_host() {
let recipesParent = yield RecipeHelpers.initNewParent();
Assert.strictEqual(recipesParent._recipesByHost.size, 0, "Initially 0 recipes");
recipesParent.add({
hosts: ["example.com"],
});
Assert.strictEqual(recipesParent._recipesByHost.size, 1,
"Check number of hosts after the addition");
let exampleRecipes = recipesParent.getRecipesForHost("example.com");
Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
let recipe = [...exampleRecipes][0];
Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host");
});
add_task(function* test_add_get_non_standard_port_host() {
let recipesParent = yield RecipeHelpers.initNewParent();
recipesParent.add({
hosts: ["example.com:8080"],
});
Assert.strictEqual(recipesParent._recipesByHost.size, 1,
"Check number of hosts after the addition");
let exampleRecipes = recipesParent.getRecipesForHost("example.com:8080");
Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com:8080");
let recipe = [...exampleRecipes][0];
Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
Assert.strictEqual(recipe.hosts[0], "example.com:8080", "Check the one host");
});
add_task(function* test_add_multiple_hosts() {
let recipesParent = yield RecipeHelpers.initNewParent();
recipesParent.add({
hosts: ["example.com", "foo.invalid"],
});
Assert.strictEqual(recipesParent._recipesByHost.size, 2,
"Check number of hosts after the addition");
let exampleRecipes = recipesParent.getRecipesForHost("example.com");
Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
let recipe = [...exampleRecipes][0];
Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
Assert.strictEqual(recipe.hosts.length, 2, "Check that two hosts are present");
Assert.strictEqual(recipe.hosts[0], "example.com", "Check the first host");
Assert.strictEqual(recipe.hosts[1], "foo.invalid", "Check the second host");
let fooRecipes = recipesParent.getRecipesForHost("foo.invalid");
Assert.strictEqual(fooRecipes.size, 1, "Check recipe count for foo.invalid");
let fooRecipe = [...fooRecipes][0];
Assert.strictEqual(fooRecipe, recipe, "Check that the recipe is shared");
Assert.strictEqual(typeof(fooRecipe), "object", "Check recipe type");
Assert.strictEqual(fooRecipe.hosts.length, 2, "Check that two hosts are present");
Assert.strictEqual(fooRecipe.hosts[0], "example.com", "Check the first host");
Assert.strictEqual(fooRecipe.hosts[1], "foo.invalid", "Check the second host");
});
add_task(function* test_add_pathRegex() {
let recipesParent = yield RecipeHelpers.initNewParent();
recipesParent.add({
hosts: ["example.com"],
pathRegex: /^\/mypath\//,
});
Assert.strictEqual(recipesParent._recipesByHost.size, 1,
"Check number of hosts after the addition");
let exampleRecipes = recipesParent.getRecipesForHost("example.com");
Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
let recipe = [...exampleRecipes][0];
Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host");
Assert.strictEqual(recipe.pathRegex.toString(), "/^\\/mypath\\//", "Check the pathRegex");
});
add_task(function* test_add_selectors() {
let recipesParent = yield RecipeHelpers.initNewParent();
recipesParent.add({
hosts: ["example.com"],
usernameSelector: "#my-username",
passwordSelector: "#my-form > input.password",
});
Assert.strictEqual(recipesParent._recipesByHost.size, 1,
"Check number of hosts after the addition");
let exampleRecipes = recipesParent.getRecipesForHost("example.com");
Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
let recipe = [...exampleRecipes][0];
Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host");
Assert.strictEqual(recipe.usernameSelector, "#my-username", "Check the usernameSelector");
Assert.strictEqual(recipe.passwordSelector, "#my-form > input.password", "Check the passwordSelector");
});
/* Begin checking errors with add */
add_task(function* test_add_missing_prop() {
let recipesParent = yield RecipeHelpers.initNewParent();
Assert.throws(() => recipesParent.add({}), /required/, "Some properties are required");
});
add_task(function* test_add_unknown_prop() {
let recipesParent = yield RecipeHelpers.initNewParent();
Assert.throws(() => recipesParent.add({
unknownProp: true,
}), /supported/, "Unknown properties should cause an error to help with typos");
});
add_task(function* test_add_invalid_hosts() {
let recipesParent = yield RecipeHelpers.initNewParent();
Assert.throws(() => recipesParent.add({
hosts: 404,
}), /array/, "hosts should be an array");
});
add_task(function* test_add_empty_host_array() {
let recipesParent = yield RecipeHelpers.initNewParent();
Assert.throws(() => recipesParent.add({
hosts: [],
}), /array/, "hosts should be a non-empty array");
});
add_task(function* test_add_pathRegex_non_regexp() {
let recipesParent = yield RecipeHelpers.initNewParent();
Assert.throws(() => recipesParent.add({
hosts: ["example.com"],
pathRegex: "foo",
}), /regular expression/, "pathRegex should be a RegExp");
});
add_task(function* test_add_usernameSelector_non_string() {
let recipesParent = yield RecipeHelpers.initNewParent();
Assert.throws(() => recipesParent.add({
hosts: ["example.com"],
usernameSelector: 404,
}), /string/, "usernameSelector should be a string");
});
add_task(function* test_add_passwordSelector_non_string() {
let recipesParent = yield RecipeHelpers.initNewParent();
Assert.throws(() => recipesParent.add({
hosts: ["example.com"],
passwordSelector: 404,
}), /string/, "passwordSelector should be a string");
});
/* End checking errors with add */
@@ -0,0 +1,41 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test filtering recipes in LoginRecipesContent.
*/
"use strict";
Cu.import("resource://testing-common/httpd.js");
Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/devtools/Console.jsm");
add_task(function* test_getFieldOverrides() {
let recipes = new Set([
{ // path doesn't match but otherwise good
hosts: ["example.com:8080"],
passwordSelector: "#password",
pathRegex: /^\/$/,
usernameSelector: ".username",
},
{ // match with no field overrides
hosts: ["example.com:8080"],
},
{ // best match (field selectors + path match)
description: "best match",
hosts: ["a.invalid", "example.com:8080", "other.invalid"],
passwordSelector: "#password",
pathRegex: /^\/first\/second\/$/,
usernameSelector: ".username",
},
]);
let form = RecipeHelpers.createTestForm("http://localhost:8080/first/second/");
let override = LoginRecipesContent.getFieldOverrides(recipes, form);
Assert.strictEqual(override.description, "best match",
"Check the best field override recipe was returned");
Assert.strictEqual(override.usernameSelector, ".username", "Check usernameSelector");
Assert.strictEqual(override.passwordSelector, "#password", "Check passwordSelector");
});
@@ -423,7 +423,7 @@ LoginTest.deleteFile(OS.Constants.Path.profileDir, filename + ".corrupt");
LoginTest.deleteFile(OS.Constants.Path.profileDir, filename);
} catch (e) {
throw "FAILED in test #" + testnum + " -- " + testdesc + ": " + e;
throw new Error("FAILED in test #" + testnum + " -- " + testdesc + ": " + e);
}
});
@@ -23,5 +23,7 @@ skip-if = os != "android"
[test_logins_metainfo.js]
[test_logins_search.js]
[test_notifications.js]
[test_recipes_add.js]
[test_recipes_content.js]
[test_storage.js]
[test_telemetry.js]