LaterRun.sys.mjs (6172B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const kEnabledPref = "browser.laterrun.enabled"; 6 const kPagePrefRoot = "browser.laterrun.pages."; 7 // Number of sessions we've been active in 8 const kSessionCountPref = "browser.laterrun.bookkeeping.sessionCount"; 9 // Time the profile was created at in seconds: 10 const kProfileCreationTime = "browser.laterrun.bookkeeping.profileCreationTime"; 11 // Time the update was applied at in seconds: 12 const kUpdateAppliedTime = "browser.laterrun.bookkeeping.updateAppliedTime"; 13 14 // After 50 sessions or 1 month since install, assume we will no longer be 15 // interested in showing anything to "new" users 16 const kSelfDestructSessionLimit = 50; 17 const kSelfDestructHoursLimit = 31 * 24; 18 19 class Page { 20 constructor({ 21 pref, 22 minimumHoursSinceInstall, 23 minimumSessionCount, 24 requireBoth, 25 url, 26 }) { 27 this.pref = pref; 28 this.minimumHoursSinceInstall = minimumHoursSinceInstall || 0; 29 this.minimumSessionCount = minimumSessionCount || 1; 30 this.requireBoth = requireBoth || false; 31 this.url = url; 32 } 33 34 get hasRun() { 35 return Services.prefs.getBoolPref(this.pref + "hasRun", false); 36 } 37 38 applies(sessionInfo) { 39 if (this.hasRun) { 40 return false; 41 } 42 if (this.requireBoth) { 43 return ( 44 sessionInfo.sessionCount >= this.minimumSessionCount && 45 sessionInfo.hoursSinceInstall >= this.minimumHoursSinceInstall 46 ); 47 } 48 return ( 49 sessionInfo.sessionCount >= this.minimumSessionCount || 50 sessionInfo.hoursSinceInstall >= this.minimumHoursSinceInstall 51 ); 52 } 53 } 54 55 export let LaterRun = { 56 get ENABLE_REASON_NEW_PROFILE() { 57 return 1; 58 }, 59 get ENABLE_REASON_UPDATE_APPLIED() { 60 return 2; 61 }, 62 63 init(reason) { 64 if (!this.enabled) { 65 return; 66 } 67 68 if (reason == this.ENABLE_REASON_NEW_PROFILE) { 69 // If this is the first run, set the time we were installed 70 if ( 71 Services.prefs.getPrefType(kProfileCreationTime) == 72 Ci.nsIPrefBranch.PREF_INVALID 73 ) { 74 // We need to store seconds in order to fit within int prefs. 75 Services.prefs.setIntPref( 76 kProfileCreationTime, 77 Math.floor(Date.now() / 1000) 78 ); 79 } 80 this.sessionCount++; 81 } else if (reason == this.ENABLE_REASON_UPDATE_APPLIED) { 82 Services.prefs.setIntPref( 83 kUpdateAppliedTime, 84 Math.floor(Services.startup.getStartupInfo().start.getTime() / 1000) 85 ); 86 } 87 88 if ( 89 this.hoursSinceInstall > kSelfDestructHoursLimit || 90 this.sessionCount > kSelfDestructSessionLimit 91 ) { 92 this.selfDestruct(); 93 } 94 }, 95 96 // The enabled, hoursSinceInstall and sessionCount properties mirror the 97 // preferences system, and are here for convenience. 98 get enabled() { 99 return Services.prefs.getBoolPref(kEnabledPref, false); 100 }, 101 102 enable(reason) { 103 if (!this.enabled) { 104 Services.prefs.setBoolPref(kEnabledPref, true); 105 this.init(reason); 106 } 107 }, 108 109 get hoursSinceInstall() { 110 let installStampSec = Services.prefs.getIntPref( 111 kProfileCreationTime, 112 Date.now() / 1000 113 ); 114 return Math.floor((Date.now() / 1000 - installStampSec) / 3600); 115 }, 116 117 get hoursSinceUpdate() { 118 let updateStampSec = Services.prefs.getIntPref(kUpdateAppliedTime, 0); 119 return Math.floor((Date.now() / 1000 - updateStampSec) / 3600); 120 }, 121 122 get sessionCount() { 123 if (this._sessionCount) { 124 return this._sessionCount; 125 } 126 return (this._sessionCount = Services.prefs.getIntPref( 127 kSessionCountPref, 128 0 129 )); 130 }, 131 132 set sessionCount(val) { 133 this._sessionCount = val; 134 Services.prefs.setIntPref(kSessionCountPref, val); 135 }, 136 137 // Because we don't want to keep incrementing this indefinitely for no reason, 138 // we will turn ourselves off after a set amount of time/sessions (see top of 139 // file). 140 selfDestruct() { 141 Services.prefs.setBoolPref(kEnabledPref, false); 142 }, 143 144 // Create an array of Page objects based on the currently set prefs 145 readPages() { 146 // Enumerate all the pages. 147 let allPrefsForPages = Services.prefs.getChildList(kPagePrefRoot); 148 let pageDataStore = new Map(); 149 for (let pref of allPrefsForPages) { 150 let [slug, prop] = pref.substring(kPagePrefRoot.length).split("."); 151 if (!pageDataStore.has(slug)) { 152 pageDataStore.set(slug, { 153 pref: pref.substring(0, pref.length - prop.length), 154 }); 155 } 156 if (prop == "requireBoth" || prop == "hasRun") { 157 pageDataStore.get(slug)[prop] = Services.prefs.getBoolPref(pref, false); 158 } else if (prop == "url") { 159 pageDataStore.get(slug)[prop] = Services.prefs.getStringPref(pref, ""); 160 } else { 161 pageDataStore.get(slug)[prop] = Services.prefs.getIntPref(pref, 0); 162 } 163 } 164 let rv = []; 165 for (let [, pageData] of pageDataStore) { 166 if (pageData.url) { 167 let urlString = Services.urlFormatter.formatURL(pageData.url.trim()); 168 let uri = URL.parse(urlString)?.URI; 169 if (!uri) { 170 console.error( 171 "Invalid LaterRun page URL ", 172 pageData.url, 173 " ignored." 174 ); 175 continue; 176 } 177 if (!uri.schemeIs("https")) { 178 console.error("Insecure LaterRun page URL ", uri.spec, " ignored."); 179 } else { 180 pageData.url = uri.spec; 181 rv.push(new Page(pageData)); 182 } 183 } 184 } 185 return rv; 186 }, 187 188 // Return a URL for display as a 'later run' page if its criteria are matched, 189 // or null otherwise. 190 // NB: will only return one page at a time; if multiple pages match, it's up 191 // to the preference service which one gets shown first, and the next one 192 // will be shown next startup instead. 193 getURL() { 194 if (!this.enabled) { 195 return null; 196 } 197 let pages = this.readPages(); 198 let page = pages.find(p => p.applies(this)); 199 if (page) { 200 Services.prefs.setBoolPref(page.pref + "hasRun", true); 201 return page.url; 202 } 203 return null; 204 }, 205 }; 206 207 LaterRun.init();