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