diff options
Diffstat (limited to 'browser/modules/LaterRun.sys.mjs')
-rw-r--r-- | browser/modules/LaterRun.sys.mjs | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/browser/modules/LaterRun.sys.mjs b/browser/modules/LaterRun.sys.mjs new file mode 100644 index 0000000000..505942d617 --- /dev/null +++ b/browser/modules/LaterRun.sys.mjs @@ -0,0 +1,209 @@ +/* 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(); |