Source code

Revision control

Copy as Markdown

Other Tools

/* Copyright 2022 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GeckoViewActorParent } from "resource://gre/modules/GeckoViewActorParent.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PdfJsTelemetry: "resource://pdf.js/PdfJsTelemetry.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
class FindHandler {
#browser;
#callbacks = null;
#state = null;
constructor(aBrowser) {
this.#browser = aBrowser;
}
cleanup() {
this.#state = null;
this.#callbacks = null;
}
onEvent(aEvent, aData, aCallback) {
if (
this.#browser.contentPrincipal.spec !==
"resource://pdf.js/web/viewer.html"
) {
return;
}
debug`onEvent: name=${aEvent}, data=${aData}`;
switch (aEvent) {
case "GeckoView:ClearMatches":
this.cleanup();
this.#browser.sendMessageToActor(
"PDFJS:Child:handleEvent",
{
type: "findbarclose",
detail: null,
},
"GeckoViewPdfjs"
);
break;
case "GeckoView:DisplayMatches":
if (!this.#state) {
return;
}
this.#browser.sendMessageToActor(
"PDFJS:Child:handleEvent",
{
type: "findhighlightallchange",
detail: this.#state,
},
"GeckoViewPdfjs"
);
break;
case "GeckoView:FindInPage":
const type = this.#getFindType(aData);
this.#browser.sendMessageToActor(
"PDFJS:Child:handleEvent",
{
type,
detail: this.#state,
},
"GeckoViewPdfjs"
);
this.#callbacks.push([aCallback, this.#state]);
break;
}
}
#getFindType(aData) {
const newState = {
query: this.#state?.query,
caseSensitive: !!aData.matchCase,
entireWord: !!aData.wholeWord,
highlightAll: !!aData.highlightAll,
findPrevious: !!aData.backwards,
matchDiacritics: !!aData.matchDiacritics,
};
if (!this.#state) {
// It's a new search.
newState.query = aData.searchString;
this.#state = newState;
this.#callbacks = [];
return "find";
}
if (aData.searchString && this.#state.query !== aData.searchString) {
// The searched string has changed.
newState.query = aData.searchString;
this.#state = newState;
return "find";
}
for (const [key, type] of [
["caseSensitive", "findcasesensitivitychange"],
["entireWord", "findentirewordchange"],
["matchDiacritics", "finddiacriticmatchingchange"],
]) {
if (this.#state[key] !== newState[key]) {
this.#state = newState;
return type;
}
}
this.#state = newState;
return "findagain";
}
updateMatchesCount(aData, aResult) {
if (
(aResult !== Ci.nsITypeAheadFind.FIND_FOUND &&
aResult !== Ci.nsITypeAheadFind.FIND_WRAPPED) ||
!this.#state
) {
return;
}
if (this.#callbacks.length === 0) {
warn`There are no callbacks to use to set the matches count.`;
return;
}
const [callback, state] = this.#callbacks.shift();
aData ||= { current: 0, total: -1 };
const response = {
found: aData.total !== 0,
wrapped:
aData.total === 0 || aResult === Ci.nsITypeAheadFind.FIND_WRAPPED,
current: aData.current,
total: aData.total,
searchString: state.query,
linkURL: null,
clientRect: null,
flags: {
backwards: state.findPrevious,
matchCase: state.caseSensitive,
wholeWord: state.entireWord,
},
};
callback.onSuccess(response);
}
}
class FileSaver {
#browser;
#callback = null;
#eventDispatcher;
constructor(aBrowser, eventDispatcher) {
this.#browser = aBrowser;
this.#eventDispatcher = eventDispatcher;
}
cleanup() {
this.#callback = null;
}
onEvent(aEvent, aData, aCallback) {
if (
this.#browser.contentPrincipal.spec !==
"resource://pdf.js/web/viewer.html"
) {
return;
}
lazy.PdfJsTelemetry.onGeckoview("save_as_pdf_tapped");
this.#callback = aCallback;
this.#browser.sendMessageToActor(
"PDFJS:Child:handleEvent",
{
type: "save",
},
"GeckoViewPdfjs"
);
}
async save({ blobUrl, filename, originalUrl }) {
try {
const isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
this.#browser
);
if (this.#callback) {
// "Save as PDF" from the share menu.
this.#callback.onSuccess({
url: blobUrl,
filename,
originalUrl,
isPrivate,
});
} else {
// "Download" or "Open in app" from the pdf.js toolbar.
this.#eventDispatcher.sendRequest({
type: "GeckoView:SavePdf",
url: blobUrl,
filename,
originalUrl,
isPrivate,
skipConfirmation: true,
requestExternalApp: false,
});
}
lazy.PdfJsTelemetry.onGeckoview("download_succeeded");
} catch (e) {
lazy.PdfJsTelemetry.onGeckoview("download_failed");
if (this.#callback) {
this.#callback?.onError(`Cannot save the pdf: ${e}.`);
} else {
warn`Cannot save the pdf because of: ${e}`;
}
} finally {
this.cleanup();
}
}
}
export class GeckoViewPdfjsParent extends GeckoViewActorParent {
#findHandler;
#fileSaver;
receiveMessage(aMsg) {
debug`receiveMessage: name=${aMsg.name}, data=${aMsg.data}`;
switch (aMsg.name) {
case "PDFJS:Parent:updateControlState":
return this.#updateControlState(aMsg);
case "PDFJS:Parent:updateMatchesCount":
return this.#updateMatchesCount(aMsg);
case "PDFJS:Parent:addEventListener":
return this.#addEventListener(aMsg);
case "PDFJS:Parent:saveURL":
return this.#save(aMsg);
case "PDFJS:Parent:getNimbus":
return this.#getExperimentFeature();
case "PDFJS:Parent:recordExposure":
return this.#recordExposure();
case "PDFJS:Parent:reportTelemetry":
return this.#reportTelemetry(aMsg);
default:
break;
}
return undefined;
}
didDestroy() {
debug`didDestroy`;
if (!this.#findHandler) {
return;
}
// The eventDispatcher is null when the tab is destroyed.
if (this.eventDispatcher) {
this.eventDispatcher.unregisterListener(this.#findHandler, [
"GeckoView:ClearMatches",
"GeckoView:DisplayMatches",
"GeckoView:FindInPage",
]);
this.eventDispatcher.unregisterListener(this.#fileSaver, [
"GeckoView:PDFSave",
]);
}
this.#findHandler.cleanup();
this.#findHandler = null;
this.#fileSaver.cleanup();
this.#fileSaver = null;
}
#addEventListener({ data: { aSupportsFind } }) {
this.#fileSaver = new FileSaver(this.browser, this.eventDispatcher);
this.eventDispatcher.registerListener(this.#fileSaver, [
"GeckoView:PDFSave",
]);
if (!aSupportsFind) {
return;
}
if (this.#findHandler) {
this.#findHandler.cleanup();
return;
}
this.#findHandler = new FindHandler(this.browser);
this.eventDispatcher.registerListener(this.#findHandler, [
"GeckoView:ClearMatches",
"GeckoView:DisplayMatches",
"GeckoView:FindInPage",
]);
}
#updateMatchesCount({ data }) {
this.#findHandler.updateMatchesCount(data, Ci.nsITypeAheadFind.FIND_FOUND);
}
#updateControlState({ data: { matchesCount, result } }) {
this.#findHandler.updateMatchesCount(matchesCount, result);
}
#save({ data }) {
this.#fileSaver.save(data);
}
async #getExperimentFeature() {
let result = null;
try {
const experimentActor = this.window.moduleManager.getActor(
"GeckoViewExperimentDelegate"
);
result = await experimentActor.getExperimentFeature("pdfjs");
} catch (e) {
warn`Cannot get experiment feature: ${e}`;
}
this.sendAsyncMessage("PDFJS:Child:getNimbus", result);
}
async #recordExposure() {
try {
const experimentActor = this.window.moduleManager.getActor(
"GeckoViewExperimentDelegate"
);
await experimentActor.recordExposure("pdfjs");
} catch (e) {
warn`Cannot record experiment exposure: ${e}`;
}
}
#reportTelemetry(aMsg) {
lazy.PdfJsTelemetry.report(aMsg.data);
}
}
const { debug, warn } = GeckoViewPdfjsParent.initLogging(
"GeckoViewPdfjsParent"
);