Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
const kEnabledPref = "browser.laterrun.enabled";
const kPagePrefRoot = "browser.laterrun.pages.";
// Number of sessions we've been active in
const kSessionCountPref = "browser.laterrun.bookkeeping.sessionCount";
// Time the profile was created at in seconds:
const kProfileCreationTime = "browser.laterrun.bookkeeping.profileCreationTime";
// Time the update was applied at in seconds:
const kUpdateAppliedTime = "browser.laterrun.bookkeeping.updateAppliedTime";
// After 50 sessions or 1 month since install, assume we will no longer be
// interested in showing anything to "new" users
const kSelfDestructSessionLimit = 50;
const kSelfDestructHoursLimit = 31 * 24;
class Page {
constructor({
pref,
minimumHoursSinceInstall,
minimumSessionCount,
requireBoth,
url,
}) {
this.pref = pref;
this.minimumHoursSinceInstall = minimumHoursSinceInstall || 0;
this.minimumSessionCount = minimumSessionCount || 1;
this.requireBoth = requireBoth || false;
this.url = url;
}
get hasRun() {
return Services.prefs.getBoolPref(this.pref + "hasRun", false);
}
applies(sessionInfo) {
if (this.hasRun) {
return false;
}
if (this.requireBoth) {
return (
sessionInfo.sessionCount >= this.minimumSessionCount &&
sessionInfo.hoursSinceInstall >= this.minimumHoursSinceInstall
);
}
return (
sessionInfo.sessionCount >= this.minimumSessionCount ||
sessionInfo.hoursSinceInstall >= this.minimumHoursSinceInstall
);
}
}
export let LaterRun = {
get ENABLE_REASON_NEW_PROFILE() {
return 1;
},
get ENABLE_REASON_UPDATE_APPLIED() {
return 2;
},
init(reason) {
if (!this.enabled) {
return;
}
if (reason == this.ENABLE_REASON_NEW_PROFILE) {
// If this is the first run, set the time we were installed
if (
Services.prefs.getPrefType(kProfileCreationTime) ==
Ci.nsIPrefBranch.PREF_INVALID
) {
// We need to store seconds in order to fit within int prefs.
Services.prefs.setIntPref(
kProfileCreationTime,
Math.floor(Date.now() / 1000)
);
}
this.sessionCount++;
} else if (reason == this.ENABLE_REASON_UPDATE_APPLIED) {
Services.prefs.setIntPref(
kUpdateAppliedTime,
Math.floor(Services.startup.getStartupInfo().start.getTime() / 1000)
);
}
if (
this.hoursSinceInstall > kSelfDestructHoursLimit ||
this.sessionCount > kSelfDestructSessionLimit
) {
this.selfDestruct();
}
},
// The enabled, hoursSinceInstall and sessionCount properties mirror the
// preferences system, and are here for convenience.
get enabled() {
return Services.prefs.getBoolPref(kEnabledPref, false);
},
enable(reason) {
if (!this.enabled) {
Services.prefs.setBoolPref(kEnabledPref, true);
this.init(reason);
}
},
get hoursSinceInstall() {
let installStampSec = Services.prefs.getIntPref(
kProfileCreationTime,
Date.now() / 1000
);
return Math.floor((Date.now() / 1000 - installStampSec) / 3600);
},
get hoursSinceUpdate() {
let updateStampSec = Services.prefs.getIntPref(kUpdateAppliedTime, 0);
return Math.floor((Date.now() / 1000 - updateStampSec) / 3600);
},
get sessionCount() {
if (this._sessionCount) {
return this._sessionCount;
}
return (this._sessionCount = Services.prefs.getIntPref(
kSessionCountPref,
0
));
},
set sessionCount(val) {
this._sessionCount = val;
Services.prefs.setIntPref(kSessionCountPref, val);
},
// Because we don't want to keep incrementing this indefinitely for no reason,
// we will turn ourselves off after a set amount of time/sessions (see top of
// file).
selfDestruct() {
Services.prefs.setBoolPref(kEnabledPref, false);
},
// Create an array of Page objects based on the currently set prefs
readPages() {
// Enumerate all the pages.
let allPrefsForPages = Services.prefs.getChildList(kPagePrefRoot);
let pageDataStore = new Map();
for (let pref of allPrefsForPages) {
let [slug, prop] = pref.substring(kPagePrefRoot.length).split(".");
if (!pageDataStore.has(slug)) {
pageDataStore.set(slug, {
pref: pref.substring(0, pref.length - prop.length),
});
}
if (prop == "requireBoth" || prop == "hasRun") {
pageDataStore.get(slug)[prop] = Services.prefs.getBoolPref(pref, false);
} else if (prop == "url") {
pageDataStore.get(slug)[prop] = Services.prefs.getStringPref(pref, "");
} else {
pageDataStore.get(slug)[prop] = Services.prefs.getIntPref(pref, 0);
}
}
let rv = [];
for (let [, pageData] of pageDataStore) {
if (pageData.url) {
let uri = null;
try {
let urlString = Services.urlFormatter.formatURL(pageData.url.trim());
uri = Services.io.newURI(urlString);
} catch (ex) {
console.error(
"Invalid LaterRun page URL ",
pageData.url,
" ignored."
);
continue;
}
if (!uri.schemeIs("https")) {
console.error("Insecure LaterRun page URL ", uri.spec, " ignored.");
} else {
pageData.url = uri.spec;
rv.push(new Page(pageData));
}
}
}
return rv;
},
// Return a URL for display as a 'later run' page if its criteria are matched,
// or null otherwise.
// NB: will only return one page at a time; if multiple pages match, it's up
// to the preference service which one gets shown first, and the next one
// will be shown next startup instead.
getURL() {
if (!this.enabled) {
return null;
}
let pages = this.readPages();
let page = pages.find(p => p.applies(this));
if (page) {
Services.prefs.setBoolPref(page.pref + "hasRun", true);
return page.url;
}
return null;
},
};
LaterRun.init();