Bug 1899117 - Add file and input type telemetry for EXIF metadata stripper project. r=bvandersloot,anti-tracking-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D226524
This commit is contained in:
Leander Schwarz
2024-10-29 12:49:37 +00:00
parent b67d2dbcc9
commit 7fec00d6b7
20 changed files with 1109 additions and 0 deletions

View File

@@ -7,6 +7,7 @@
#include "mozilla/dom/ClipboardEvent.h"
#include "mozilla/ContentEvents.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/ImageInputTelemetry.h"
#include "mozilla/dom/DataTransfer.h"
#include "nsIClipboard.h"
@@ -74,6 +75,17 @@ DataTransfer* ClipboardEvent::GetClipboardData() {
}
}
// Only collect image input telemetry on external paste events, unless in
// automation. In tests we skip this check using the pref, since we cannot
// synthesize external events.
if ((!mEventIsInternal ||
StaticPrefs::privacy_imageInputTelemetry_enableTestMode()) &&
!mImageInputTelemetryCollected) {
ImageInputTelemetry::MaybeRecordPasteImageInputTelemetry(
event, GetParentObject()->PrincipalOrNull());
mImageInputTelemetryCollected = true;
}
return event->mClipboardData;
}

View File

@@ -35,6 +35,9 @@ class ClipboardEvent : public Event {
void InitClipboardEvent(const nsAString& aType, bool aCanBubble,
bool aCancelable, DataTransfer* aClipboardData);
private:
bool mImageInputTelemetryCollected = false;
protected:
~ClipboardEvent() = default;
};

View File

@@ -6,6 +6,7 @@
#include "mozilla/dom/DragEvent.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/ImageInputTelemetry.h"
#include "mozilla/MouseEvents.h"
#include "nsContentUtils.h"
#include "prtime.h"
@@ -64,6 +65,17 @@ DataTransfer* DragEvent::GetDataTransfer() {
NS_ENSURE_SUCCESS(rv, nullptr);
}
// Only collect image input telemetry on external drop events, unless in
// automation. In tests we skip this check using the pref, since we cannot
// synthesize external events.
if ((!mEventIsInternal ||
StaticPrefs::privacy_imageInputTelemetry_enableTestMode()) &&
!mImageInputTelemetryCollected) {
ImageInputTelemetry::MaybeRecordDropImageInputTelemetry(
dragEvent, GetParentObject()->PrincipalOrNull());
mImageInputTelemetryCollected = true;
}
return dragEvent->mDataTransfer;
}

View File

@@ -42,6 +42,9 @@ class DragEvent : public MouseEvent {
const nsAString& aType,
const DragEventInit& aParam);
private:
bool mImageInputTelemetryCollected = false;
protected:
~DragEvent() = default;
};

View File

@@ -28,6 +28,7 @@
#include "mozilla/dom/WheelEventBinding.h"
#include "mozilla/dom/WindowGlobalChild.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/ImageInputTelemetry.h"
#include "mozilla/Maybe.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/PresShell.h"
@@ -488,6 +489,8 @@ HTMLInputElement::nsFilePickerShownCallback::Done(
OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
element->SetAsFile() = domBlob->ToFile();
ImageInputTelemetry::MaybeRecordFilePickerImageInputTelemetry(domBlob);
}
} else {
MOZ_ASSERT(mode == nsIFilePicker::modeOpen ||
@@ -537,6 +540,8 @@ HTMLInputElement::nsFilePickerShownCallback::Done(
OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
element->SetAsFile() = file;
ImageInputTelemetry::MaybeRecordFilePickerImageInputTelemetry(blob);
} else if (tmp) {
RefPtr<Directory> directory = static_cast<Directory*>(tmp.get());
OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();

View File

@@ -15214,6 +15214,13 @@
value: false
mirror: always
# To be used in automated test environments to enable event telemetry
# collection.
- name: privacy.imageInputTelemetry.enableTestMode
type: bool
value: false
mirror: always
#---------------------------------------------------------------------------
# Prefs starting with "prompts."
#---------------------------------------------------------------------------

View File

@@ -0,0 +1,173 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ImageInputTelemetry.h"
#include "mozilla/ContentEvents.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/dom/Blob.h"
#include "mozilla/dom/DataTransfer.h"
#include "mozilla/dom/FileList.h"
#include "mozilla/glean/GleanMetrics.h"
#include "nsIPrincipal.h"
namespace mozilla {
// Known MIME types as listed in nsMimeTypes.h + HEIC types
static constexpr nsLiteralCString kKnownImageMIMETypes[] = {
"image/gif"_ns,
"image/jpeg"_ns,
"image/jpg"_ns,
"image/pjpeg"_ns,
"image/png"_ns,
"image/apng"_ns,
"image/x-png"_ns,
"image/x-portable-pixmap"_ns,
"image/x-xbitmap"_ns,
"image/x-xbm"_ns,
"image/xbm"_ns,
"image/x-jg"_ns,
"image/tiff"_ns,
"image/bmp"_ns,
"image/x-ms-bmp"_ns,
"image/x-ms-clipboard-bmp"_ns,
"image/x-icon"_ns,
"image/vnd.microsoft.icon"_ns,
"image/icon"_ns, // "video/x-mng"_ns, - This is checked in
// ImageInputTelemetry::IsKnownImageMIMEType()
"image/x-jng"_ns,
"image/svg+xml"_ns,
"image/webp"_ns,
"image/avif"_ns,
"image/jxl"_ns,
// Additionally added HEIC image formats due to mobile popularity
"image/heic"_ns,
"image/heif"_ns,
};
/* private static */ bool ImageInputTelemetry::IsKnownImageMIMEType(
const nsCString& aInputFileType) {
if (aInputFileType.Find("video/x-mng"_ns) != kNotFound) {
return true;
}
if (aInputFileType.Find("image/"_ns) != kNotFound) {
for (const nsCString& knownImageMIMEType : kKnownImageMIMETypes) {
if (aInputFileType.Equals(knownImageMIMEType)) {
return true;
}
}
}
return false;
}
/* private static */ bool ImageInputTelemetry::IsContentPrincipal(
nsIPrincipal* aContentPrincipal) {
bool isSystemPrincipal;
aContentPrincipal->GetIsSystemPrincipal(&isSystemPrincipal);
return !(isSystemPrincipal || aContentPrincipal->SchemeIs("about"));
}
/* private static */ void ImageInputTelemetry::RecordImageInputTelemetry(
const nsCString& aImageType, ImageInputType aInputType) {
nsAutoCString inputType;
switch (aInputType) {
case ImageInputType::Drop:
inputType = "Drop"_ns;
break;
case ImageInputType::Paste:
inputType = "Paste"_ns;
break;
case ImageInputType::FilePicker:
inputType = "FilePicker"_ns;
break;
}
glean::image_input_telemetry::ImageInputExtra extra = {
.imageType = Some(aImageType),
.inputType = Some(inputType),
};
glean::image_input_telemetry::image_input.Record(Some(extra));
}
/* private static */ void ImageInputTelemetry::MaybeRecordImageInputTelemetry(
ImageInputType aInputType, dom::DataTransfer* aDataTransfer) {
// Check if input datatransfers contains files.
RefPtr<dom::FileList> files =
aDataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal());
if (!files) {
return;
}
nsAutoString fileType;
nsCString fileTypeC;
for (uint32_t i = 0; i < files->Length(); i++) {
dom::File* file = files->Item(i);
if (!file) {
continue;
}
file->GetType(fileType);
fileTypeC = NS_ConvertUTF16toUTF8(fileType);
if (IsKnownImageMIMEType(fileTypeC)) {
ImageInputTelemetry::RecordImageInputTelemetry(fileTypeC, aInputType);
}
}
}
/* static */ void ImageInputTelemetry::MaybeRecordDropImageInputTelemetry(
WidgetDragEvent* aDragEvent, nsIPrincipal* aContentPrincipal) {
MOZ_ASSERT(aDragEvent);
MOZ_ASSERT(aContentPrincipal);
// Only collect telemetry when drag data is accessed on drop.
if (aDragEvent->mMessage != eDrop || !aDragEvent->mDataTransfer) {
return;
}
// Only process events on content, not about pages, e.g. default drop
// handler displays dropped file.
if (!IsContentPrincipal(aContentPrincipal)) {
return;
}
ImageInputTelemetry::MaybeRecordImageInputTelemetry(
ImageInputType::Drop, aDragEvent->mDataTransfer);
}
/* static */ void ImageInputTelemetry::MaybeRecordPasteImageInputTelemetry(
InternalClipboardEvent* aClipboardEvent, nsIPrincipal* aContentPrincipal) {
MOZ_ASSERT(aClipboardEvent);
MOZ_ASSERT(aContentPrincipal);
// Only collect telemetry when clipboard data is accessed on paste.
if (aClipboardEvent->mMessage != ePaste || !aClipboardEvent->mClipboardData) {
return;
}
// Only process events on content, neither system (e.g. URL bar) nor
// about pages (e.g. searchbar in about:preferences).
if (!IsContentPrincipal(aContentPrincipal)) {
return;
}
ImageInputTelemetry::MaybeRecordImageInputTelemetry(
ImageInputType::Paste, aClipboardEvent->mClipboardData);
}
/* static */ void ImageInputTelemetry::MaybeRecordFilePickerImageInputTelemetry(
dom::Blob* aFilePickerBlob) {
MOZ_ASSERT(aFilePickerBlob);
nsAutoString fileType;
nsCString fileTypeC;
aFilePickerBlob->GetType(fileType);
fileTypeC = NS_ConvertUTF16toUTF8(fileType);
if (IsKnownImageMIMEType(fileTypeC)) {
ImageInputTelemetry::RecordImageInputTelemetry(fileTypeC,
ImageInputType::FilePicker);
}
}
} // namespace mozilla

View File

@@ -0,0 +1,71 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_imageinputtypetelemetry_h
#define mozilla_imageinputtypetelemetry_h
#include "nsStringFwd.h"
class nsIPrincipal;
namespace mozilla {
class InternalClipboardEvent;
class WidgetDragEvent;
namespace dom {
class Blob;
class DataTransfer;
} // namespace dom
// Class for collection of image input file type and input type telemetry.
class ImageInputTelemetry {
public:
enum ImageInputType {
Drop,
Paste,
FilePicker,
};
// Collect telemetry on drop of user image input to the browser. This is
// collected in DragEvent:GetDataTransfer(). Telemetry is not collected
// for about:* pages (e.g. newtab) and for system principals (e.g. drop on
// about:preferences).
static void MaybeRecordDropImageInputTelemetry(
WidgetDragEvent* aDragEvent, nsIPrincipal* aContentPrincipal);
// Collect telemetry on paste of user image input to the browser. This is
// collected in ClipboardEvent:GetClipboardData(). Telemetry is not collected
// for about:* pages (e.g. searchbar in about:preferences) and for system
// principals (e.g. paste to urlbar).
static void MaybeRecordPasteImageInputTelemetry(
InternalClipboardEvent* aClipboardEvent, nsIPrincipal* aContentPrincipal);
// Collect telemetry on user selection of image files through the FilePicker.
// The telemetry is collected in
// HTMLInputElement::nsFilePickerShownCallback::Done(), both for single and
// multiple collected files.
static void MaybeRecordFilePickerImageInputTelemetry(
dom::Blob* aFilePickerBlob);
private:
ImageInputTelemetry() = default;
~ImageInputTelemetry() = default;
// This function returns true if kKnownImageMIMETypes contains aFileInputType.
static bool IsKnownImageMIMEType(const nsCString& aInputFileType);
// This function returns true if aContentPincipal is of any page possibly
// collectiing user data (including local e.g. html pages).
static bool IsContentPrincipal(nsIPrincipal* aContentPrincipal);
// Iterate through input files from aDataTransfer and record a Glean event if
// the file is an image of a type contained in kKnownImageMIMETypes.
static void MaybeRecordImageInputTelemetry(ImageInputType aInputType,
dom::DataTransfer* aDataTransfer);
// Wrapper for recording an image_input Glean event containing the image_type
// and the input_type (FilePicker, Paste, Drag).
static void RecordImageInputTelemetry(const nsCString& aImageType,
ImageInputType aInputType);
};
} // namespace mozilla
#endif // mozilla_imageinputtypetelemetry_h

View File

@@ -0,0 +1,34 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# Adding a new metric? We have docs for that!
# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
---
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
$tags:
- 'Core :: Privacy: Anti-Tracking'
image.input.telemetry:
image_input:
type: event
description: >
Type of user image file(s) input and browser input type and platform.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1899117
data_reviews:
- https://phabricator.services.mozilla.com/D223316
data_sensitivity:
- interaction
notification_emails:
- lschwarz@mozilla.com
- bvandersloot@mozilla.com
expires: 139
extra_keys:
image_type:
description: User input image file type(s).
type: string
input_type:
description: User input type.
type: string

View File

@@ -0,0 +1,24 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
with Files("**"):
BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking")
EXPORTS.mozilla += [
"ImageInputTelemetry.h",
]
UNIFIED_SOURCES += [
"ImageInputTelemetry.cpp",
]
FINAL_LIBRARY = "xul"
BROWSER_CHROME_MANIFESTS += [
"test/browser/browser.toml",
]
MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.toml"]

View File

@@ -0,0 +1,9 @@
[DEFAULT]
head = ["head.js"]
prefs = ["privacy.imageInputTelemetry.enableTestMode=true", "browser.newtab.preload=false"]
["browser_dragimage_telemetry.js"]
["browser_filepicker_telemetry.js"]
["browser_pasteimage_telemetry.js"]

View File

@@ -0,0 +1,245 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use stric";
async function createDropEventAndSetFiles(browser, files, addListener = true) {
await SpecialPowers.spawn(
browser,
[files, addListener],
async (files, addListener) => {
async function onDrop(event) {
// Access DataTransfer so DragEvent::GetDataTransfer() is triggered.
event.dataTransfer;
}
const dataTransfer = new content.DataTransfer();
dataTransfer.dropEffect = "copy";
files.forEach(file => {
dataTransfer.items.add(file);
});
if (addListener) {
content.document.addEventListener("drop", onDrop);
}
let event = content.document.createEvent("DragEvent");
event.initDragEvent(
"drop",
false,
false,
content.window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
content.document.body,
dataTransfer
);
content.document.dispatchEvent(event);
if (addListener) {
content.document.removeEventListener("drop", onDrop);
}
}
);
}
add_task(async function drag_single_image() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab("example.com", async browser => {
const files = new File(["some data"], "hello.jpg", { type: "image/jpg" });
await createDropEventAndSetFiles(browser, [files]);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot.length, 1, "Glean - One telemetry event gathered!");
is(snapshot[0].name, "image_input", "Glean - Received correct event!");
is(
snapshot[0].extra.image_type,
"image/jpg",
"Glean - Event extra correct image type!"
);
is(
snapshot[0].extra.input_type,
"Drop",
"Glean - Event extra correct input type!"
);
// Cleanup
Services.fog.testResetFOG();
});
add_task(async function drag_single_image_about() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab("about:newtab", async browser => {
const files = new File(["some data"], "hello.jpg", { type: "image/jpg" });
await createDropEventAndSetFiles(browser, [files], false);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(
snapshot,
null,
"Glean - No telemetry event gathered as expected on about:newtab!"
);
// Cleanup
Services.fog.testResetFOG();
});
// Separate test since about:* pages use system principal
add_task(async function drag_single_image_system() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab("about:preferences", async browser => {
const files = new File(["some data"], "hello.jpg", { type: "image/jpg" });
await createDropEventAndSetFiles(browser, [files]);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(
snapshot,
null,
"Glean - No telemetry event gathered as expected on other about:* pages!"
);
// Cleanup
Services.fog.testResetFOG();
});
add_task(async function drag_single_txt() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab("example.com", async browser => {
const files = new File(["some data"], "hello.txt", { type: "text/plain" });
await createDropEventAndSetFiles(browser, [files]);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - No telemetry collected for txt file!");
// Cleanup
Services.fog.testResetFOG();
});
add_task(async function drag_multiple_images() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab("example.com", async browser => {
let files = [];
kKnownImageMIMETypes.forEach(mimeType => {
const image = new File(["some data"], "hello", { type: mimeType });
files.push(image);
});
await createDropEventAndSetFiles(browser, files);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(
snapshot.length,
kKnownImageMIMETypes.length,
"Glean - Collected telemetry for all known image types!"
);
snapshot.forEach(function (event, i) {
let eventMimeType = event.extra.image_type;
is(
kKnownImageMIMETypes[i],
eventMimeType,
"Glean - Correct MIME-Type: " + eventMimeType
);
is("Drop", event.extra.input_type, "Glean - Correct input type!");
});
// Cleanup
Services.fog.testResetFOG();
});
add_task(async function drag_multiple_mixed() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab("example.com", async browser => {
const image1 = new File(["some data"], "A", { type: "plain/text" });
const txt1 = new File(["some data"], "B", { type: "image/png" });
const image2 = new File(["some data"], "C", { type: "plain/text" });
const txt2 = new File(["some data"], "D", { type: "image/bmp" });
await createDropEventAndSetFiles(browser, [image1, txt1, image2, txt2]);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot.length, 2, "Glean - Correct number of events recorded!");
is(
snapshot[0].extra.image_type,
"image/png",
"Glean - Correct first image type!"
);
is(snapshot[0].extra.input_type, "Drop", "Glean - Correct first input type!");
is(
snapshot[1].extra.image_type,
"image/bmp",
"Glean - Correct second image type!"
);
is(
snapshot[1].extra.input_type,
"Drop",
"Glean - Correct second input type!"
);
// Cleanup
Services.fog.testResetFOG();
});
add_task(async function drag_multiple_txt() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab("example.com", async browser => {
const txt1 = new File(["some data"], "A", { type: "plain/text" });
const txt2 = new File(["some data"], "B", { type: "plain/text" });
const txt3 = new File(["some data"], "C", { type: "plain/text" });
const txt4 = new File(["some data"], "D", { type: "plain/text" });
await createDropEventAndSetFiles(browser, [txt1, txt2, txt3, txt4]);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - No non-image input events collected!");
// Cleanup
Services.fog.testResetFOG();
});

View File

@@ -0,0 +1,192 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const PAGE = `
<!DOCTYPE HTML>
<html>
<body>
<input id="modeOpen" type=file></input>
<input id="modeOpenMultiple" type=file multiple></input>
</body>
</html>
`;
const INPUT_URL = "data:text/html," + encodeURI(PAGE);
async function openFilePickerAndSetFiles(browser, files) {
await SpecialPowers.spawn(browser, [files], async files => {
let filePicker = content.SpecialPowers.MockFilePicker;
filePicker.cleanup();
filePicker.init(content.browsingContext);
let filePickerShow = new Promise(resolve => {
filePicker.showCallback = () => {
filePicker.setFiles(files);
filePicker.returnValue = filePicker.returnOk;
info("MockFilePicker shown, files set!");
resolve();
};
});
let input =
files.length > 1
? content.document.getElementById("modeOpenMultiple")
: content.document.getElementById("modeOpen");
content.document.notifyUserGestureActivation();
input.click();
return filePickerShow;
});
}
add_task(async function filepicker_single_image() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab(INPUT_URL, async browser => {
const files = new File(["some data"], "hello.jpg", { type: "image/jpg" });
await openFilePickerAndSetFiles(browser, [files]);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot.length, 1, "Glean - One telemertry event gathered!");
is(snapshot[0].name, "image_input", "Glean - Received correct event!");
is(
snapshot[0].extra.image_type,
"image/jpg",
"Glean - Event extra correct image type!"
);
is(
snapshot[0].extra.input_type,
"FilePicker",
"Glean - Event extra correct input type!"
);
// Cleanup
Services.fog.testResetFOG();
});
add_task(async function filepicker_single_txt() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab(INPUT_URL, async browser => {
const files = new File(["some data"], "hello.txt", {
type: "plain/text",
});
await openFilePickerAndSetFiles(browser, [files]);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - No telemetry collected for txt file!");
// Cleanup
Services.fog.testResetFOG();
});
add_task(async function filepicker_multiple_images() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab(INPUT_URL, async browser => {
let files = [];
kKnownImageMIMETypes.forEach(mimeType => {
const image = new File(["some data"], "hello", { type: mimeType });
files.push(image);
});
await openFilePickerAndSetFiles(browser, files);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(
snapshot.length,
kKnownImageMIMETypes.length,
"Glean - Collected telemetry for all known image types!"
);
snapshot.forEach(function (event, i) {
let eventMimeType = event.extra.image_type;
is(
kKnownImageMIMETypes[i],
eventMimeType,
"Glean - Correct MIME-Type: " + eventMimeType
);
is("FilePicker", event.extra.input_type, "Glean - Correct input type!");
});
// Cleanup
Services.fog.testResetFOG();
});
add_task(async function filepicker_multiple_mixed() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab(INPUT_URL, async browser => {
const image1 = new File(["some data"], "A", { type: "plain/text" });
const txt1 = new File(["some data"], "B", { type: "image/png" });
const image2 = new File(["some data"], "C", { type: "plain/text" });
const txt2 = new File(["some data"], "D", { type: "image/bmp" });
await openFilePickerAndSetFiles(browser, [image1, txt1, image2, txt2]);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot.length, 2, "Glean - Correct number of events recorded!");
is(
snapshot[0].extra.image_type,
"image/png",
"Glean - Correct first image type!"
);
is(
snapshot[0].extra.input_type,
"FilePicker",
"Glean - Correct first input type!"
);
is(
snapshot[1].extra.image_type,
"image/bmp",
"Glean - Correct second image type!"
);
is(
snapshot[1].extra.input_type,
"FilePicker",
"Glean - Correct second input type!"
);
// Cleanup
Services.fog.testResetFOG();
});
add_task(async function filepicker_multiple_txt() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await BrowserTestUtils.withNewTab(INPUT_URL, async browser => {
const txt1 = new File(["some data"], "A", { type: "plain/text" });
const txt2 = new File(["some data"], "B", { type: "plain/text" });
const txt3 = new File(["some data"], "C", { type: "plain/text" });
const txt4 = new File(["some data"], "D", { type: "plain/text" });
await openFilePickerAndSetFiles(browser, [txt1, txt2, txt3, txt4]);
await Services.fog.testFlushAllChildren();
});
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - No non-image input events collected!");
// Cleanup
Services.fog.testResetFOG();
});

View File

@@ -0,0 +1,182 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use stric";
async function setClipboard(file) {
const tmp = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
tmp.initWithPath(file);
const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
Ci.nsITransferable
);
trans.init(null);
trans.addDataFlavor("application/x-moz-file");
trans.setTransferData("application/x-moz-file", tmp);
// Write to clipboard.
Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
}
async function createPasteEvent(browser, addListener = true) {
await SpecialPowers.spawn(browser, [addListener], async addListener => {
function onPaste(event) {
// Access DataTransfer so DragEvent::GetDataTransfer() is triggered.
event.clipboardData;
if (addListener) {
content.document.removeEventListener("paste", onPaste);
}
}
if (addListener) {
content.document.addEventListener("paste", onPaste);
}
});
await BrowserTestUtils.synthesizeKey("v", { accelKey: true }, browser);
}
add_task(async function paste_single_image() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"example.com"
);
let browser = tab.linkedBrowser;
const file = await IOUtils.createUniqueFile(
PathUtils.tempDir,
"test-file.jpg",
0o600
);
await setClipboard(file);
await createPasteEvent(browser);
await Services.fog.testFlushAllChildren();
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot.length, 1, "Glean - One telemetry event gathered!");
is(snapshot[0].name, "image_input", "Glean - Received correct event!");
is(
snapshot[0].extra.image_type,
"image/jpeg",
"Glean - Event extra correct image type!"
);
is(
snapshot[0].extra.input_type,
"Paste",
"Glean - Event extra correct input type!"
);
// Cleanup
await IOUtils.remove(file);
BrowserTestUtils.removeTab(tab);
Services.fog.testResetFOG();
});
add_task(async function paste_single_image_about() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"about:newtab"
);
let browser = tab.linkedBrowser;
const file = await IOUtils.createUniqueFile(
PathUtils.tempDir,
"test-file.jpg",
0o600
);
await setClipboard(file);
await createPasteEvent(browser, false);
await Services.fog.testFlushAllChildren();
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(
snapshot,
null,
"Glean - No telemetry event gathered as expected on about:newtab!"
);
// Cleanup
await IOUtils.remove(file);
BrowserTestUtils.removeTab(tab);
Services.fog.testResetFOG();
});
// Separate test since about:* pages use system principal
add_task(async function paste_single_image_system() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"about:preferences"
);
let browser = tab.linkedBrowser;
const file = await IOUtils.createUniqueFile(
PathUtils.tempDir,
"test-file.jpg",
0o600
);
await setClipboard(file);
await createPasteEvent(browser);
await Services.fog.testFlushAllChildren();
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(
snapshot,
null,
"Glean - No telemetry event gathered as expected on other about:* pages!"
);
// Cleanup
await IOUtils.remove(file);
BrowserTestUtils.removeTab(tab);
Services.fog.testResetFOG();
});
add_task(async function paste_single_txt() {
let snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"example.com"
);
let browser = tab.linkedBrowser;
const file = await IOUtils.createUniqueFile(
PathUtils.tempDir,
"test-file.txt",
0o600
);
await IOUtils.writeUTF8(file, "Hello World!");
await setClipboard(file);
await createPasteEvent(browser);
await Services.fog.testFlushAllChildren();
snapshot = Glean.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - No telemetry collected for txt file!");
// Cleanup
await IOUtils.remove(file);
BrowserTestUtils.removeTab(tab);
Services.fog.testResetFOG();
});

View File

@@ -0,0 +1,34 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Known MIME types as listed in nsMimeTypes.h
const kKnownImageMIMETypes = [
"image/gif",
"image/jpeg",
"image/jpg",
"image/pjpeg",
"image/png",
"image/apng",
"image/x-png",
"image/x-portable-pixmap",
"image/x-xbitmap",
"image/x-xbm",
"image/xbm",
"image/x-jg",
"image/tiff",
"image/bmp",
"image/x-ms-bmp",
"image/x-ms-clipboard-bmp",
"image/x-icon",
"image/vnd.microsoft.icon",
"image/icon",
"video/x-mng",
"image/x-jng",
"image/svg+xml",
"image/webp",
"image/avif",
"image/jxl",
"image/heic",
"image/heif",
];

View File

@@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Bug 1799977: Using workaround to test telemetry in plain mochitests
const GleanTest = new Proxy(
{},
{
// eslint-disable-next-line no-unused-vars
get(target, categoryName, receiver) {
return new Proxy(
{},
{
// eslint-disable-next-line no-shadow
// eslint-disable-next-line no-unused-vars
get(target, metricName, receiver) {
return {
async testGetValue() {
return SpecialPowers.spawnChrome(
[categoryName, metricName],
// eslint-disable-next-line no-shadow
async (categoryName, metricName) => {
await Services.fog.testFlushAllChildren();
const window = this.browsingContext.topChromeWindow;
let snapshot =
window.Glean[categoryName][metricName].testGetValue();
await Services.fog.testResetFOG();
return snapshot;
}
);
},
};
},
}
);
},
}
);

View File

@@ -0,0 +1,5 @@
[DEFAULT]
support-files = ["head.js"]
run-if = ["os == 'android'"]
["test_geckoview_filepicker_telemetry.html"]

View File

@@ -0,0 +1,56 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tests for GeckoView Image Input Telemetry</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="head.js" type="application/javascript"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<input type="file"></input>
<script class="testbody" type="text/javascript">
async function openFilePicker(window) {
const image = new File(["some data"], "hello.jpg", { type: "image/jpg" });
const txt = new File(["some data"], "hello.txt", { type: "plain/txt" });
const files = [ image, txt]
await SpecialPowers.spawn(window, [files], async files => {
let filePicker = content.SpecialPowers.MockFilePicker;
filePicker.init(content.browsingContext);
let filePickerShow = new Promise(resolve => {
filePicker.showCallback = () => {
filePicker.setFiles(files);
filePicker.returnValue = filePicker.returnOk;
info("MockFilePicker shown, files set!");
resolve();
};
});
let input = content.document.querySelector("input[type=file]");
content.document.notifyUserGestureActivation();
input.click();
return filePickerShow;
});
}
add_task(async function test_imageinput_telemetry() {
let snapshot = await GleanTest.imageInputTelemetry.imageInput.testGetValue();
is(snapshot, null, "Glean - Initially no event gathered!");
await openFilePicker(window);
snapshot = await GleanTest.imageInputTelemetry.imageInput.testGetValue();
is(snapshot.length, 1, "Glean - One telemetry event gathered!");
is(snapshot[0].name, "image_input", "Glean - Received correct event!");
is(snapshot[0].extra.image_type, "image/jpg", "Glean - Event extra correct image type!");
is(snapshot[0].extra.input_type, "FilePicker", "Glean - Event extra correct input type!");
});
</script>
</body>
</html>

View File

@@ -11,6 +11,7 @@ JAR_MANIFESTS += ["jar.mn"]
DIRS += [
"bouncetrackingprotection",
"imageinputmetadatastripper",
]
XPIDL_SOURCES += [

View File

@@ -50,6 +50,7 @@ gecko_metrics = [
"services/common/metrics.yaml",
"services/sync/modules/metrics.yaml",
"toolkit/components/antitracking/bouncetrackingprotection/metrics.yaml",
"toolkit/components/antitracking/imageinputmetadatastripper/metrics.yaml",
"toolkit/components/antitracking/metrics.yaml",
"toolkit/components/cookiebanners/metrics.yaml",
"toolkit/components/downloads/metrics.yaml",