TabState.sys.mjs (7773B)
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 lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.sys.mjs", 9 TabAttributes: "resource:///modules/sessionstore/TabAttributes.sys.mjs", 10 TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", 11 sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs", 12 }); 13 14 /** 15 * Module that contains tab state collection methods. 16 */ 17 class _TabState { 18 /** 19 * Processes a data update sent by the content script. 20 */ 21 update(permanentKey, { data }) { 22 lazy.TabStateCache.update(permanentKey, data); 23 } 24 25 /** 26 * Collect data related to a single tab, synchronously. 27 * 28 * @param tab 29 * tabbrowser tab 30 * @param [extData] 31 * optional dictionary object, containing custom tab values. 32 * 33 * @returns {TabStateData} An object with the data for this tab. If the 34 * tab has not been invalidated since the last call to 35 * collect(aTab), the same object is returned. 36 */ 37 collect(tab, extData) { 38 return this.#collectBaseTabData(tab, { extData }); 39 } 40 41 /** 42 * Collect data related to a single tab, including private data. 43 * Use with caution. 44 * 45 * @param tab 46 * tabbrowser tab 47 * @param [extData] 48 * optional dictionary object, containing custom tab values. 49 * 50 * @returns {object} An object with the data for this tab. This data is never 51 * cached, it will always be read from the tab and thus be 52 * up-to-date. 53 */ 54 clone(tab, extData) { 55 return this.#collectBaseTabData(tab, { extData, includePrivateData: true }); 56 } 57 58 /** 59 * Collects basic tab data for a given tab. 60 * 61 * @param tab 62 * tabbrowser tab 63 * @param options (object) 64 * {extData: object} optional dictionary object, containing custom tab values 65 * {includePrivateData: true} to always include private data 66 * 67 * @returns {TabStateData} An object with the basic data for this tab. 68 */ 69 #collectBaseTabData(tab, options) { 70 let tabData = { entries: [], lastAccessed: tab.lastAccessed }; 71 let browser = tab.linkedBrowser; 72 73 if (tab.pinned) { 74 tabData.pinned = true; 75 } 76 77 tabData.hidden = tab.hidden; 78 79 if (browser.audioMuted) { 80 tabData.muted = true; 81 tabData.muteReason = tab.muteReason; 82 } 83 84 if (tab.group) { 85 tabData.groupId = tab.group.id; 86 } 87 88 tabData.searchMode = tab.ownerGlobal.gURLBar.getSearchMode(browser, true); 89 90 tabData.userContextId = tab.userContextId || 0; 91 92 // Save tab attributes. 93 tabData.attributes = lazy.TabAttributes.get(tab); 94 95 if (options.extData) { 96 tabData.extData = options.extData; 97 } 98 99 // Copy data from the tab state cache only if the tab has fully finished 100 // restoring. We don't want to overwrite data contained in __SS_data. 101 this.copyFromCache(browser.permanentKey, tabData, options); 102 103 // After copyFromCache() was called we check for properties that are kept 104 // in the cache only while the tab is pending or restoring. Once that 105 // happened those properties will be removed from the cache and will 106 // be read from the tab/browser every time we collect data. 107 108 // Store the tab icon. 109 if (!("image" in tabData)) { 110 let tabbrowser = tab.ownerGlobal.gBrowser; 111 tabData.image = tabbrowser.getIcon(tab); 112 } 113 114 // If there is a userTypedValue set, then either the user has typed something 115 // in the URL bar, or a new tab was opened with a URI to load. 116 // If so, we also track whether we were still in the process of loading something. 117 if (!("userTypedValue" in tabData) && browser.userTypedValue) { 118 tabData.userTypedValue = browser.userTypedValue; 119 // We always used to keep track of the loading state as an integer, where 120 // '0' indicated the user had typed since the last load (or no load was 121 // ongoing), and any positive value indicated we had started a load since 122 // the last time the user typed in the URL bar. Mimic this to keep the 123 // session store representation in sync, even though we now represent this 124 // more explicitly: 125 tabData.userTypedClear = browser.didStartLoadSinceLastUserTyping() 126 ? 1 127 : 0; 128 } 129 130 return tabData; 131 } 132 133 processAboutRestartrequiredEnties(aEntries) { 134 // Find if there are some entries that matches (contains) the 135 // about:restartrequired page. It can be plain about:restartrequired 136 // or something more complicated like about:restartrequired?e=restartrequired&u=about%3Ablank&c=UTF-8&d=%20 137 if ( 138 !aEntries.some(e => e.url && e.url.startsWith("about:restartrequired")) 139 ) { 140 return aEntries; 141 } 142 143 // now we need a deep copy 144 let newEntries = structuredClone(aEntries); 145 newEntries.forEach((item, index, object) => { 146 if (item.url === "about:restartrequired") { 147 object.splice(index, 1); 148 } else if (item.url.startsWith("about:restartrequired")) { 149 try { 150 const parsedURL = new URL(item.url); 151 if (parsedURL && parsedURL.searchParams.has("u")) { 152 const previousURL = parsedURL.searchParams.get("u"); 153 object[index].url = previousURL; 154 } 155 } catch (ex) { 156 lazy.sessionStoreLogger.error( 157 `Exception when parsing "${item.url}"`, 158 ex 159 ); 160 } 161 } 162 }); 163 164 return newEntries; 165 } 166 167 /** 168 * Copy data for the given |browser| from the cache to |tabData|. 169 * 170 * @param permanentKey (object) 171 * The browser belonging to the given |tabData| object. 172 * @param tabData (object) 173 * The tab data belonging to the given |tab|. 174 * @param options (object) 175 * {includePrivateData: true} to always include private data 176 */ 177 copyFromCache(permanentKey, tabData, options = {}) { 178 let data = lazy.TabStateCache.get(permanentKey); 179 if (!data) { 180 return; 181 } 182 183 // The caller may explicitly request to omit privacy checks. 184 let includePrivateData = options && options.includePrivateData; 185 186 for (let key of Object.keys(data)) { 187 let value = data[key]; 188 189 // Filter sensitive data according to the current privacy level. 190 if (!includePrivateData) { 191 if (key === "storage") { 192 value = lazy.PrivacyFilter.filterSessionStorageData(value); 193 } else if (key === "formdata") { 194 value = lazy.PrivacyFilter.filterFormData(value); 195 } 196 } 197 198 if (key === "history") { 199 // Make a shallow copy of the entries array. We (currently) don't update 200 // entries in place, so we don't have to worry about performing a deep 201 // copy. 202 tabData.entries = [...value.entries]; 203 204 if (value.hasOwnProperty("index")) { 205 tabData.index = value.index; 206 } 207 208 if (value.hasOwnProperty("requestedIndex")) { 209 tabData.requestedIndex = value.requestedIndex; 210 } 211 212 tabData.entries = this.processAboutRestartrequiredEnties(value.entries); 213 } else if (!value && (key == "scroll" || key == "formdata")) { 214 // [Bug 1554512] 215 216 // If scroll or formdata null it indicates that the update to 217 // be performed is to remove them, and not copy a null 218 // value. Scroll will be null when the position is at the top 219 // of the document, formdata will be null when there is only 220 // default data. 221 delete tabData[key]; 222 } else { 223 tabData[key] = value; 224 } 225 } 226 } 227 } 228 229 export const TabState = new _TabState();