MockFilePicker.sys.mjs (10212B)
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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 FileUtils: "resource://gre/modules/FileUtils.sys.mjs", 11 WrapPrivileged: "resource://testing-common/WrapPrivileged.sys.mjs", 12 }); 13 14 const Cm = Components.manager; 15 16 const CONTRACT_ID = "@mozilla.org/filepicker;1"; 17 18 if (import.meta.url.includes("specialpowers")) { 19 Cu.crashIfNotInAutomation(); 20 } 21 22 var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); 23 var oldClassID; 24 var newClassID = Services.uuid.generateUUID(); 25 var newFactory = function (window) { 26 return { 27 createInstance(aIID) { 28 return new MockFilePickerInstance(window).QueryInterface(aIID); 29 }, 30 QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), 31 }; 32 }; 33 34 export var MockFilePicker = { 35 returnOK: Ci.nsIFilePicker.returnOK, 36 returnCancel: Ci.nsIFilePicker.returnCancel, 37 returnReplace: Ci.nsIFilePicker.returnReplace, 38 39 filterAll: Ci.nsIFilePicker.filterAll, 40 filterHTML: Ci.nsIFilePicker.filterHTML, 41 filterText: Ci.nsIFilePicker.filterText, 42 filterImages: Ci.nsIFilePicker.filterImages, 43 filterXML: Ci.nsIFilePicker.filterXML, 44 filterXUL: Ci.nsIFilePicker.filterXUL, 45 filterApps: Ci.nsIFilePicker.filterApps, 46 filterAllowURLs: Ci.nsIFilePicker.filterAllowURLs, 47 filterAudio: Ci.nsIFilePicker.filterAudio, 48 filterVideo: Ci.nsIFilePicker.filterVideo, 49 50 window: null, 51 pendingPromises: [], 52 53 init(browsingContext) { 54 if (registrar.isCIDRegistered(newClassID)) { 55 this.cleanup(); 56 } else { 57 this.reset(); 58 } 59 60 this.window = browsingContext.window; 61 this.factory = newFactory(this.window); 62 oldClassID = registrar.contractIDToCID(CONTRACT_ID); 63 registrar.registerFactory(newClassID, "", CONTRACT_ID, this.factory); 64 }, 65 66 reset() { 67 this.appendFilterCallback = null; 68 this.appendFiltersCallback = null; 69 this.displayDirectory = null; 70 this.displaySpecialDirectory = ""; 71 this.filterIndex = 0; 72 this.mode = null; 73 this.returnData = []; 74 this.returnValue = null; 75 this.returnDataForWebKitDirs = []; 76 this.showCallback = null; 77 this.afterOpenCallback = null; 78 this.shown = false; 79 this.showing = false; 80 }, 81 82 cleanup() { 83 var previousFactory = this.factory; 84 this.reset(); 85 this.factory = null; 86 this.window = null; 87 if (oldClassID) { 88 registrar.unregisterFactory(newClassID, previousFactory); 89 registrar.registerFactory(oldClassID, "", CONTRACT_ID, null); 90 } 91 }, 92 93 internalFileData(obj) { 94 return { 95 nsIFile: "nsIFile" in obj ? obj.nsIFile : null, 96 domFile: "domFile" in obj ? obj.domFile : null, 97 domDirectory: "domDirectory" in obj ? obj.domDirectory : null, 98 }; 99 }, 100 101 useAnyFile() { 102 var file = lazy.FileUtils.getDir("TmpD", []); 103 file.append("testfile"); 104 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); 105 let promise = this.window.File.createFromNsIFile(file) 106 .then( 107 domFile => domFile, 108 () => null 109 ) 110 // domFile can be null. 111 .then(domFile => { 112 this.returnData = [this.internalFileData({ nsIFile: file, domFile })]; 113 }) 114 .then(() => file); 115 116 this.pendingPromises = [promise]; 117 118 // We return a promise in order to support some existing mochitests. 119 return promise; 120 }, 121 122 useBlobFile() { 123 var blob = new this.window.Blob([]); 124 var file = new this.window.File([blob], "helloworld.txt", { 125 type: "plain/text", 126 }); 127 this.returnData = [this.internalFileData({ domFile: file })]; 128 this.pendingPromises = []; 129 }, 130 131 useDirectory(aPath) { 132 var directory = new this.window.Directory(aPath); 133 var file = new lazy.FileUtils.File(aPath); 134 this.returnData = [ 135 this.internalFileData({ domDirectory: directory, nsIFile: file }), 136 ]; 137 this.pendingPromises = []; 138 this.returnDataForWebKitDirs = []; 139 140 if (AppConstants.platform === "android") { 141 for (const filename of ["/foo.txt", "/subdir/bar.txt"]) { 142 let fileInDir = lazy.FileUtils.File(aPath + filename); 143 this.window.File.createFromNsIFile(file, { 144 existenceCheck: false, 145 }).then(domFile => { 146 this.returnDataForWebKitDirs.push( 147 this.internalFileData({ nsIFile: fileInDir, domFile }) 148 ); 149 }); 150 // promise might be added into pendinngPromise, but it causes 151 // InvalidStateError. 152 } 153 } 154 }, 155 156 setFiles(files) { 157 this.returnData = []; 158 this.pendingPromises = []; 159 160 for (let file of files) { 161 if (this.window.File.isInstance(file)) { 162 this.returnData.push(this.internalFileData({ domFile: file })); 163 } else { 164 let promise = this.window.File.createFromNsIFile(file, { 165 existenceCheck: false, 166 }); 167 168 promise.then(domFile => { 169 this.returnData.push( 170 this.internalFileData({ nsIFile: file, domFile }) 171 ); 172 }); 173 this.pendingPromises.push(promise); 174 } 175 } 176 }, 177 178 getNsIFile() { 179 if (this.returnData.length >= 1) { 180 return this.returnData[0].nsIFile; 181 } 182 return null; 183 }, 184 }; 185 186 function MockFilePickerInstance(window) { 187 this.window = window; 188 this.showCallback = null; 189 this.showCallbackWrapped = null; 190 } 191 MockFilePickerInstance.prototype = { 192 QueryInterface: ChromeUtils.generateQI(["nsIFilePicker"]), 193 init(aParent, aTitle, aMode) { 194 this.mode = aMode; 195 this.filterIndex = MockFilePicker.filterIndex; 196 this.parent = aParent; 197 }, 198 appendFilter(aTitle, aFilter) { 199 if (typeof MockFilePicker.appendFilterCallback == "function") { 200 MockFilePicker.appendFilterCallback(this, aTitle, aFilter); 201 } 202 }, 203 appendFilters(aFilterMask) { 204 if (typeof MockFilePicker.appendFiltersCallback == "function") { 205 MockFilePicker.appendFiltersCallback(this, aFilterMask); 206 } 207 }, 208 defaultString: "", 209 defaultExtension: "", 210 parent: null, 211 filterIndex: 0, 212 displayDirectory: null, 213 displaySpecialDirectory: "", 214 get file() { 215 if (MockFilePicker.returnData.length >= 1) { 216 return MockFilePicker.returnData[0].nsIFile; 217 } 218 219 return null; 220 }, 221 222 // We don't support directories here. 223 get domFileOrDirectory() { 224 if (MockFilePicker.returnData.length < 1) { 225 return null; 226 } 227 228 if (MockFilePicker.returnData[0].domFile) { 229 return MockFilePicker.returnData[0].domFile; 230 } 231 232 if (MockFilePicker.returnData[0].domDirectory) { 233 return MockFilePicker.returnData[0].domDirectory; 234 } 235 236 return null; 237 }, 238 get fileURL() { 239 if ( 240 MockFilePicker.returnData.length >= 1 && 241 MockFilePicker.returnData[0].nsIFile 242 ) { 243 return Services.io.newFileURI(MockFilePicker.returnData[0].nsIFile); 244 } 245 246 return null; 247 }, 248 *getFiles(asDOM) { 249 for (let d of MockFilePicker.returnData) { 250 if (asDOM) { 251 yield d.domFile || d.domDirectory; 252 } else if (d.nsIFile) { 253 yield d.nsIFile; 254 } else { 255 throw Components.Exception("", Cr.NS_ERROR_FAILURE); 256 } 257 } 258 }, 259 get files() { 260 return this.getFiles(false); 261 }, 262 get domFileOrDirectoryEnumerator() { 263 return this.getFiles(true); 264 }, 265 *getDomFilesInWebKitDirectory() { 266 for (let d of MockFilePicker.returnDataForWebKitDirs) { 267 yield d.domFile; 268 } 269 }, 270 get domFilesInWebKitDirectory() { 271 if (AppConstants.platform !== "android") { 272 throw Components.Exception("", Cr.NS_ERROR_FAILURE); 273 } 274 275 return this.getDomFilesInWebKitDirectory(); 276 }, 277 open(aFilePickerShownCallback) { 278 MockFilePicker.showing = true; 279 Services.tm.dispatchToMainThread(() => { 280 // Maybe all the pending promises are already resolved, but we want to be sure. 281 Promise.all(MockFilePicker.pendingPromises) 282 .then( 283 () => { 284 return Ci.nsIFilePicker.returnOK; 285 }, 286 () => { 287 return Ci.nsIFilePicker.returnCancel; 288 } 289 ) 290 .then(result => { 291 // Nothing else has to be done. 292 MockFilePicker.pendingPromises = []; 293 294 if (result == Ci.nsIFilePicker.returnCancel) { 295 return result; 296 } 297 298 MockFilePicker.displayDirectory = this.displayDirectory; 299 MockFilePicker.displaySpecialDirectory = this.displaySpecialDirectory; 300 MockFilePicker.shown = true; 301 if (typeof MockFilePicker.showCallback == "function") { 302 if (MockFilePicker.showCallback != this.showCallback) { 303 this.showCallback = MockFilePicker.showCallback; 304 if (Cu.isXrayWrapper(this.window)) { 305 this.showCallbackWrapped = lazy.WrapPrivileged.wrapCallback( 306 MockFilePicker.showCallback, 307 this.window 308 ); 309 } else { 310 this.showCallbackWrapped = this.showCallback; 311 } 312 } 313 try { 314 var returnValue = this.showCallbackWrapped(this); 315 if (typeof returnValue != "undefined") { 316 return returnValue; 317 } 318 } catch (ex) { 319 return Ci.nsIFilePicker.returnCancel; 320 } 321 } 322 323 return MockFilePicker.returnValue; 324 }) 325 .then(result => { 326 // Some additional result file can be set by the callback. Let's 327 // resolve the pending promises again. 328 return Promise.all(MockFilePicker.pendingPromises).then( 329 () => { 330 return result; 331 }, 332 () => { 333 return Ci.nsIFilePicker.returnCancel; 334 } 335 ); 336 }) 337 .then(result => { 338 MockFilePicker.pendingPromises = []; 339 340 if (aFilePickerShownCallback) { 341 aFilePickerShownCallback.done(result); 342 } 343 344 if (typeof MockFilePicker.afterOpenCallback == "function") { 345 Services.tm.dispatchToMainThread(() => { 346 MockFilePicker.afterOpenCallback(this); 347 }); 348 } 349 }); 350 }); 351 }, 352 };