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