1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7"use strict";
8
9const EXPORTED_SYMBOLS = ["EnigmailFiles"];
10
11const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
12const { XPCOMUtils } = ChromeUtils.import(
13  "resource://gre/modules/XPCOMUtils.jsm"
14);
15
16XPCOMUtils.defineLazyModuleGetters(this, {
17  AppConstants: "resource://gre/modules/AppConstants.jsm",
18  EnigmailData: "chrome://openpgp/content/modules/data.jsm",
19  EnigmailStreams: "enigmail/streams.jsm",
20  EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
21});
22
23const NS_RDONLY = 0x01;
24const NS_WRONLY = 0x02;
25const NS_CREATE_FILE = 0x08;
26const NS_TRUNCATE = 0x20;
27const DEFAULT_FILE_PERMS = 0o600;
28
29var EnigmailFiles = {
30  isAbsolutePath(filePath, isDosLike) {
31    // Check if absolute path
32    if (isDosLike) {
33      return (
34        filePath.search(/^\w+:\\/) === 0 ||
35        filePath.search(/^\\\\/) === 0 ||
36        filePath.search(/^\/\//) === 0
37      );
38    }
39
40    return filePath.search(/^\//) === 0;
41  },
42
43  resolvePath(filePath, envPath, isDosLike) {
44    EnigmailLog.DEBUG("files.jsm: resolvePath: filePath=" + filePath + "\n");
45
46    if (EnigmailFiles.isAbsolutePath(filePath, isDosLike)) {
47      return filePath;
48    }
49
50    if (!envPath) {
51      return null;
52    }
53
54    const fileNames = filePath.split(";");
55
56    const pathDirs = envPath.split(isDosLike ? ";" : ":");
57
58    for (let i = 0; i < fileNames.length; i++) {
59      for (let j = 0; j < pathDirs.length; j++) {
60        try {
61          const pathDir = Cc["@mozilla.org/file/local;1"].createInstance(
62            Ci.nsIFile
63          );
64
65          EnigmailLog.DEBUG(
66            "files.jsm: resolvePath: checking for " +
67              pathDirs[j] +
68              "/" +
69              fileNames[i] +
70              "\n"
71          );
72
73          EnigmailFiles.initPath(pathDir, pathDirs[j]);
74
75          try {
76            if (pathDir.exists() && pathDir.isDirectory()) {
77              pathDir.appendRelativePath(fileNames[i]);
78
79              if (pathDir.exists() && !pathDir.isDirectory()) {
80                return pathDir;
81              }
82            }
83          } catch (ex) {}
84        } catch (ex) {}
85      }
86    }
87    return null;
88  },
89
90  createFileStream(filePath, permissions) {
91    try {
92      let localFile;
93      if (typeof filePath == "string") {
94        localFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
95        EnigmailFiles.initPath(localFile, filePath);
96      } else {
97        localFile = filePath.QueryInterface(Ci.nsIFile);
98      }
99
100      if (localFile.exists()) {
101        if (localFile.isDirectory() || !localFile.isWritable()) {
102          throw Components.Exception("", Cr.NS_ERROR_FAILURE);
103        }
104
105        if (!permissions) {
106          permissions = localFile.permissions;
107        }
108      }
109
110      if (!permissions) {
111        permissions = DEFAULT_FILE_PERMS;
112      }
113
114      const flags = NS_WRONLY | NS_CREATE_FILE | NS_TRUNCATE;
115
116      const fileStream = Cc[
117        "@mozilla.org/network/file-output-stream;1"
118      ].createInstance(Ci.nsIFileOutputStream);
119
120      fileStream.init(localFile, flags, permissions, 0);
121
122      return fileStream;
123    } catch (ex) {
124      EnigmailLog.ERROR(
125        "files.jsm: createFileStream: Failed to create " + filePath + "\n"
126      );
127      return null;
128    }
129  },
130
131  // path initialization function
132  // uses persistentDescriptor in case that initWithPath fails
133  // (seems to happen frequently with UTF-8 characters in path names)
134  initPath(localFileObj, pathStr) {
135    localFileObj.initWithPath(pathStr);
136
137    if (!localFileObj.exists()) {
138      localFileObj.persistentDescriptor = pathStr;
139    }
140  },
141
142  /**
143   * Read the contents of a text file into a string
144   *
145   * @param fileObj: Object (nsIFile)
146   *
147   * @return String (file contents)
148   */
149  readFile(fileObj) {
150    let fileContents = "";
151
152    if (fileObj.exists()) {
153      let inspector = Cc["@mozilla.org/jsinspector;1"].createInstance(
154        Ci.nsIJSInspector
155      );
156
157      IOUtils.read(fileObj.path)
158        .then(arr => {
159          fileContents = EnigmailData.arrayBufferToString(arr); // Convert the array to a text
160          inspector.exitNestedEventLoop();
161        })
162        .catch(err => {
163          inspector.exitNestedEventLoop();
164        });
165
166      inspector.enterNestedEventLoop(0); // wait for async process to terminate
167    }
168
169    return fileContents;
170  },
171
172  /** Read the contents of a file with binary data into a string
173   * @param fileObj: Object (nsIFile)
174   *
175   * @return String (file contents)
176   */
177  readBinaryFile(fileObj) {
178    let fileContents = "";
179
180    if (fileObj.exists()) {
181      let inspector = Cc["@mozilla.org/jsinspector;1"].createInstance(
182        Ci.nsIJSInspector
183      );
184
185      IOUtils.read(fileObj.path)
186        .then(arr => {
187          for (let i = 0; i < arr.length; i++) {
188            fileContents += String.fromCharCode(arr[i]);
189          }
190
191          inspector.exitNestedEventLoop();
192        })
193        .catch(err => {
194          inspector.exitNestedEventLoop();
195        });
196
197      inspector.enterNestedEventLoop(0); // wait for async process to terminate
198    }
199
200    return fileContents;
201  },
202
203  formatCmdLine(command, args) {
204    function getQuoted(str) {
205      str = str.toString();
206
207      let i = str.indexOf(" ");
208      if (i >= 0) {
209        return '"' + str + '"';
210      }
211
212      return str;
213    }
214
215    if (command instanceof Ci.nsIFile) {
216      command = EnigmailFiles.getFilePathDesc(command);
217    }
218
219    const cmdStr = getQuoted(command) + " ";
220    const argStr = args
221      .map(getQuoted)
222      .join(" ")
223      .replace(/\\\\/g, "\\");
224    return cmdStr + argStr;
225  },
226
227  getFilePathDesc(nsFileObj) {
228    if (AppConstants.platform == "win") {
229      return nsFileObj.persistentDescriptor;
230    }
231
232    return nsFileObj.path;
233  },
234
235  getFilePath(nsFileObj) {
236    return EnigmailData.convertToUnicode(
237      EnigmailFiles.getFilePathDesc(nsFileObj),
238      "utf-8"
239    );
240  },
241
242  getEscapedFilename(fileNameStr) {
243    if (AppConstants.platform == "win") {
244      // escape the backslashes and the " character (for Windows)
245      fileNameStr = fileNameStr.replace(/([\\"])/g, "\\$1");
246
247      // replace leading "\\" with "//"
248      fileNameStr = fileNameStr.replace(/^\\\\*/, "//");
249    }
250    return fileNameStr;
251  },
252
253  /**
254   * get the temporary folder
255   *
256   * @return nsIFile object holding a reference to the temp directory
257   */
258  getTempDirObj() {
259    const TEMPDIR_PROP = "TmpD";
260
261    try {
262      const dsprops = Cc["@mozilla.org/file/directory_service;1"]
263        .getService()
264        .QueryInterface(Ci.nsIProperties);
265      return dsprops.get(TEMPDIR_PROP, Ci.nsIFile);
266    } catch (ex) {
267      // let's guess ...
268      const tmpDirObj = Cc["@mozilla.org/file/local;1"].createInstance(
269        Ci.nsIFile
270      );
271      if (AppConstants.platform == "win") {
272        tmpDirObj.initWithPath("C:/TEMP");
273      } else {
274        tmpDirObj.initWithPath("/tmp");
275      }
276      return tmpDirObj;
277    }
278  },
279
280  /**
281   * get the temporary folder as string
282   *
283   * @return String containing the temp directory name
284   */
285  getTempDir() {
286    return EnigmailFiles.getTempDirObj().path;
287  },
288
289  /**
290   * create a new folder as subfolder of the temporary directory
291   *
292   * @param dirName  String  - name of subfolder
293   * @param unique   Boolean - if true, the directory is guaranteed to be unique
294   *
295   * @return nsIFile object holding a reference to the created directory
296   */
297  createTempSubDir(dirName, unique = false) {
298    const localFile = EnigmailFiles.getTempDirObj().clone();
299
300    localFile.append(dirName);
301    if (unique) {
302      localFile.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 509 /* = 0775 */);
303    } else {
304      localFile.create(Ci.nsIFile.DIRECTORY_TYPE, 509 /* = 0775 */);
305    }
306
307    return localFile;
308  },
309
310  /**
311   * Ensure that a directory exists and is writeable.
312   *
313   * @param dirObj      Object - nsIFile object for the directory to test
314   * @param permissions Number - file permissions in Unix style (e.g. 0700)
315   *
316   * @return Number:
317   *    0 - OK: directory exists (or was created) and is writeable
318   *    1 - NOK: Directory does not exist (and cannot be created)
319   *    2 - NOK: Directory exists but is readonly (and cannot be modified)
320   *    3 - NOK: File object with required name exists but is not a directory
321   */
322  ensureWritableDirectory(dirObj, permissions) {
323    let retVal = -1;
324    try {
325      if (dirObj.isDirectory()) {
326        try {
327          if (dirObj.isWritable()) {
328            retVal = 0;
329          } else {
330            dirObj.permissions = permissions;
331            retVal = 0;
332          }
333        } catch (x) {
334          retVal = 2;
335        }
336      } else {
337        retVal = 3;
338      }
339    } catch (x) {
340      // directory doesn't exist
341      try {
342        dirObj.create(Ci.nsIFile.DIRECTORY_TYPE, permissions);
343        retVal = 0;
344      } catch (x2) {
345        retVal = 1;
346      }
347    }
348    return retVal;
349  },
350
351  /**
352   *  Write data to a file
353   *  @filePath |string| or |nsIFile| object - the file to be created
354   *  @data     |string|       - the data to write to the file
355   *  @permissions  |number|   - file permissions according to Unix spec (0600 by default)
356   *
357   *  @return true if data was written successfully, false otherwise
358   */
359  writeFileContents(filePath, data, permissions) {
360    try {
361      const fileOutStream = EnigmailFiles.createFileStream(
362        filePath,
363        permissions
364      );
365
366      if (data.length) {
367        if (fileOutStream.write(data, data.length) != data.length) {
368          throw Components.Exception("", Cr.NS_ERROR_FAILURE);
369        }
370
371        fileOutStream.flush();
372      }
373      fileOutStream.close();
374    } catch (ex) {
375      EnigmailLog.ERROR(
376        "files.jsm: writeFileContents: Failed to write to " + filePath + "\n"
377      );
378      return false;
379    }
380
381    return true;
382  },
383
384  /**
385   * Create a text file from the contents of a given URL
386   *
387   * @param srcUrl:  String         - the URL to download
388   * @param outFile: nsIFile object - the file to create
389   *
390   * no return value
391   */
392  writeUrlToFile(srcUrl, outFile) {
393    EnigmailLog.DEBUG("files.jsm: writeUrlToFile(" + outFile.path + ")\n");
394
395    var msgUri = Services.io.newURI(srcUrl);
396    var channel = EnigmailStreams.createChannel(msgUri);
397    var istream = channel.open();
398
399    var fstream = Cc[
400      "@mozilla.org/network/safe-file-output-stream;1"
401    ].createInstance(Ci.nsIFileOutputStream);
402    var buffer = Cc[
403      "@mozilla.org/network/buffered-output-stream;1"
404    ].createInstance(Ci.nsIBufferedOutputStream);
405    fstream.init(outFile, 0x04 | 0x08 | 0x20, 0x180, 0); // write, create, truncate
406    buffer.init(fstream, 8192);
407
408    while (istream.available() > 0) {
409      buffer.writeFrom(istream, istream.available());
410    }
411
412    // Close the output streams
413    if (buffer instanceof Ci.nsISafeOutputStream) {
414      buffer.finish();
415    } else {
416      buffer.close();
417    }
418
419    if (fstream instanceof Ci.nsISafeOutputStream) {
420      fstream.finish();
421    } else {
422      fstream.close();
423    }
424
425    // Close the input stream
426    istream.close();
427  },
428
429  // return the useable path (for gpg) of a file object
430  getFilePathReadonly(nsFileObj, creationMode) {
431    if (creationMode === null) {
432      creationMode = NS_RDONLY;
433    }
434    return nsFileObj.path;
435  },
436
437  /**
438   * Create an empty ZIP file
439   *
440   * @param nsFileObj - nsIFile object: reference to the file to be created
441   *
442   * @return nsIZipWriter object allow to perform write operations on the ZIP file
443   */
444  createZipFile(nsFileObj) {
445    const zipW = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
446    zipW.open(nsFileObj, NS_WRONLY | NS_CREATE_FILE | NS_TRUNCATE);
447
448    return zipW;
449  },
450
451  /**
452   * Open a ZIP file for reading
453   *
454   * @param nsFileObj - nsIFile object: reference to the file to be created
455   *
456   * @return nsIZipReader object allow to perform read operations on the ZIP file
457   */
458  openZipFile(nsFileObj) {
459    const zipR = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(
460      Ci.nsIZipReader
461    );
462    zipR.open(nsFileObj);
463
464    return zipR;
465  },
466
467  /**
468   * Unpack a ZIP file to a directory
469   *
470   * @param zipFile   - nsIZipReader object: file to be extracted
471   * @param targetDir - nsIFile object:      target directory
472   *
473   * @return Boolean: true if extraction successfull, false otherwise
474   */
475  extractZipFile(zipFile, targetDir) {
476    // create missing parent directories
477    function createDirWithParents(dirObj) {
478      if (!dirObj.parent.exists()) {
479        createDirWithParents(dirObj.parent);
480      }
481      dirObj.create(dirObj.DIRECTORY_TYPE, 493);
482    }
483
484    try {
485      let zipReader = EnigmailFiles.openZipFile(zipFile);
486      let f = zipReader.findEntries("*");
487
488      for (let i of f) {
489        let t = targetDir.clone();
490        let entry = zipReader.getEntry(i);
491
492        if (AppConstants.platform != "win") {
493          t.initWithPath(t.path + "/" + i);
494        } else {
495          i = i.replace(/\//g, "\\");
496          t.initWithPath(t.path + "\\" + i);
497        }
498
499        if (!t.parent.exists()) {
500          createDirWithParents(t.parent);
501        }
502
503        if (!(entry.isDirectory || i.search(/[\/\\]$/) >= 0)) {
504          zipReader.extract(i, t);
505        }
506      }
507
508      zipReader.close();
509
510      return true;
511    } catch (ex) {
512      EnigmailLog.ERROR(
513        "files.jsm: extractZipFile: Failed to create ZIP: " + ex + "\n"
514      );
515      return false;
516    }
517  },
518};
519