tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

SafariProfileMigrator.sys.mjs (22258B)


      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 { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs";
      6 
      7 import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
      8 import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
      9 
     10 const lazy = {};
     11 
     12 ChromeUtils.defineESModuleGetters(lazy, {
     13  FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
     14  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     15  PropertyListUtils: "resource://gre/modules/PropertyListUtils.sys.mjs",
     16 });
     17 
     18 // NSDate epoch is Jan 1, 2001 UTC
     19 const NS_DATE_EPOCH_MS = new Date("2001-01-01T00:00:00-00:00").getTime();
     20 
     21 // Convert NSDate timestamp to UNIX timestamp.
     22 function parseNSDate(cocoaDateStr) {
     23  let asDouble = parseFloat(cocoaDateStr);
     24  if (!isNaN(asDouble)) {
     25    return new Date(NS_DATE_EPOCH_MS + asDouble * 1000);
     26  }
     27  return new Date();
     28 }
     29 
     30 // Convert UNIX timestamp to NSDate timestamp.
     31 function msToNSDate(ms) {
     32  return parseFloat(ms - NS_DATE_EPOCH_MS) / 1000;
     33 }
     34 
     35 function Bookmarks(aBookmarksFile) {
     36  this._file = aBookmarksFile;
     37 }
     38 Bookmarks.prototype = {
     39  type: MigrationUtils.resourceTypes.BOOKMARKS,
     40 
     41  migrate: function B_migrate(aCallback) {
     42    return (async () => {
     43      let dict = await new Promise(resolve =>
     44        lazy.PropertyListUtils.read(this._file, resolve)
     45      );
     46      if (!dict) {
     47        throw new Error("Could not read Bookmarks.plist");
     48      }
     49      let children = dict.get("Children");
     50      if (!children) {
     51        throw new Error("Invalid Bookmarks.plist format");
     52      }
     53 
     54      let collection =
     55        dict.get("Title") == "com.apple.ReadingList"
     56          ? this.READING_LIST_COLLECTION
     57          : this.ROOT_COLLECTION;
     58      await this._migrateRootCollection(children, collection);
     59    })().then(
     60      () => aCallback(true),
     61      e => {
     62        console.error(e);
     63        aCallback(false);
     64      }
     65    );
     66  },
     67 
     68  // Bookmarks collections in Safari.  Constants for migrateCollection.
     69  ROOT_COLLECTION: 0,
     70  MENU_COLLECTION: 1,
     71  TOOLBAR_COLLECTION: 2,
     72  READING_LIST_COLLECTION: 3,
     73 
     74  /**
     75   * Start the migration of a Safari collection of bookmarks by retrieving favicon data.
     76   *
     77   * @param {object[]} aEntries
     78   *   The collection's children
     79   * @param {number} aCollection
     80   *   One of the _COLLECTION values above.
     81   */
     82  async _migrateRootCollection(aEntries, aCollection) {
     83    // First, try to get the favicon data of a user's bookmarks.
     84    // In Safari, Favicons are stored as files with a unique name:
     85    // the MD5 hash of the UUID of an SQLite entry in favicons.db.
     86    // Thus, we must create a map from bookmark URLs -> their favicon entry's UUID.
     87    let bookmarkURLToUUIDMap = new Map();
     88 
     89    const faviconFolder = FileUtils.getDir("ULibDir", [
     90      "Safari",
     91      "Favicon Cache",
     92    ]).path;
     93    let dbPath = PathUtils.join(faviconFolder, "favicons.db");
     94 
     95    try {
     96      // If there is an error getting favicon data, we catch the error and move on.
     97      // In this case, the bookmarkURLToUUIDMap will be left empty.
     98      let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
     99        dbPath,
    100        "Safari favicons",
    101        `SELECT I.uuid, I.url AS favicon_url, P.url
    102        FROM icon_info I
    103        INNER JOIN page_url P ON I.uuid = P.uuid;`
    104      );
    105 
    106      if (rows) {
    107        // Convert the rows from our SQLite database into a map from bookmark url to uuid
    108        for (let row of rows) {
    109          let uniqueURL = new URL(row.getResultByName("url")).href;
    110 
    111          // Normalize the URL by removing any trailing slashes. We'll make sure to do
    112          // the same when doing look-ups during a migration.
    113          if (uniqueURL.endsWith("/")) {
    114            uniqueURL = uniqueURL.replace(/\/+$/, "");
    115          }
    116          bookmarkURLToUUIDMap.set(uniqueURL, row.getResultByName("uuid"));
    117        }
    118      }
    119    } catch (ex) {
    120      console.error(ex);
    121    }
    122 
    123    await this._migrateCollection(aEntries, aCollection, bookmarkURLToUUIDMap);
    124  },
    125 
    126  /**
    127   * Recursively migrate a Safari collection of bookmarks.
    128   *
    129   * @param {object[]} aEntries
    130   *   The collection's children
    131   * @param {number} aCollection
    132   *   One of the _COLLECTION values above
    133   * @param {Map} bookmarkURLToUUIDMap
    134   *   A map from a bookmark's URL to the UUID of its entry in the favicons.db database
    135   * @returns {Promise<undefined>}
    136   *   Resolves after the bookmarks and favicons have been inserted into the
    137   *   appropriate databases.
    138   */
    139  async _migrateCollection(aEntries, aCollection, bookmarkURLToUUIDMap) {
    140    // A collection of bookmarks in Safari resembles places roots.  In the
    141    // property list files (Bookmarks.plist, ReadingList.plist) they are
    142    // stored as regular bookmarks folders, and thus can only be distinguished
    143    // from by their names and places in the hierarchy.
    144 
    145    let entriesFiltered = [];
    146    if (aCollection == this.ROOT_COLLECTION) {
    147      for (let entry of aEntries) {
    148        let type = entry.get("WebBookmarkType");
    149        if (type == "WebBookmarkTypeList" && entry.has("Children")) {
    150          let title = entry.get("Title");
    151          let children = entry.get("Children");
    152          if (title == "BookmarksBar") {
    153            await this._migrateCollection(
    154              children,
    155              this.TOOLBAR_COLLECTION,
    156              bookmarkURLToUUIDMap
    157            );
    158          } else if (title == "BookmarksMenu") {
    159            await this._migrateCollection(
    160              children,
    161              this.MENU_COLLECTION,
    162              bookmarkURLToUUIDMap
    163            );
    164          } else if (title == "com.apple.ReadingList") {
    165            await this._migrateCollection(
    166              children,
    167              this.READING_LIST_COLLECTION,
    168              bookmarkURLToUUIDMap
    169            );
    170          } else if (entry.get("ShouldOmitFromUI") !== true) {
    171            entriesFiltered.push(entry);
    172          }
    173        } else if (type == "WebBookmarkTypeLeaf") {
    174          entriesFiltered.push(entry);
    175        }
    176      }
    177    } else {
    178      entriesFiltered = aEntries;
    179    }
    180 
    181    if (!entriesFiltered.length) {
    182      return;
    183    }
    184 
    185    let folderGuid = -1;
    186    switch (aCollection) {
    187      case this.ROOT_COLLECTION: {
    188        // In Safari, it is possible (though quite cumbersome) to move
    189        // bookmarks to the bookmarks root, which is the parent folder of
    190        // all bookmarks "collections".  That is somewhat in parallel with
    191        // both the places root and the unfiled-bookmarks root.
    192        // Because the former is only an implementation detail in our UI,
    193        // the unfiled root seems to be the best choice.
    194        folderGuid = lazy.PlacesUtils.bookmarks.unfiledGuid;
    195        break;
    196      }
    197      case this.MENU_COLLECTION: {
    198        folderGuid = lazy.PlacesUtils.bookmarks.menuGuid;
    199        break;
    200      }
    201      case this.TOOLBAR_COLLECTION: {
    202        folderGuid = lazy.PlacesUtils.bookmarks.toolbarGuid;
    203        break;
    204      }
    205      case this.READING_LIST_COLLECTION: {
    206        // Reading list items are imported as regular bookmarks.
    207        // They are imported under their own folder, created either under the
    208        // bookmarks menu (in the case of startup migration).
    209        let readingListTitle = await MigrationUtils.getLocalizedString(
    210          "migration-imported-safari-reading-list"
    211        );
    212        folderGuid = (
    213          await MigrationUtils.insertBookmarkWrapper({
    214            parentGuid: lazy.PlacesUtils.bookmarks.menuGuid,
    215            type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER,
    216            title: readingListTitle,
    217          })
    218        ).guid;
    219        break;
    220      }
    221      default:
    222        throw new Error("Unexpected value for aCollection!");
    223    }
    224    if (folderGuid == -1) {
    225      throw new Error("Invalid folder GUID");
    226    }
    227 
    228    await this._migrateEntries(
    229      entriesFiltered,
    230      folderGuid,
    231      bookmarkURLToUUIDMap
    232    );
    233  },
    234 
    235  /**
    236   * Migrates bookmarks and favicons from Safari to Firefox.
    237   *
    238   * @param {object[]} entries
    239   *   The Safari collection's children
    240   * @param {number} parentGuid
    241   *   GUID of the collection folder
    242   * @param {Map} bookmarkURLToUUIDMap
    243   *   A map from a bookmark's URL to the UUID of its entry in the favicons.db database
    244   */
    245  async _migrateEntries(entries, parentGuid, bookmarkURLToUUIDMap) {
    246    let { convertedEntries, favicons } = await this._convertEntries(
    247      entries,
    248      bookmarkURLToUUIDMap
    249    );
    250 
    251    await MigrationUtils.insertManyBookmarksWrapper(
    252      convertedEntries,
    253      parentGuid
    254    );
    255 
    256    MigrationUtils.insertManyFavicons(favicons).catch(console.error);
    257  },
    258 
    259  /**
    260   * Converts Safari collection entries into a suitable format for
    261   * inserting bookmarks and favicons.
    262   *
    263   * @param {object[]} entries
    264   *   The collection's children
    265   * @param {Map} bookmarkURLToUUIDMap
    266   *   A map from a bookmark's URL to the UUID of its entry in the favicons.db database
    267   * @returns {object[]}
    268   *   Returns an object with an array of converted bookmark entries and favicons
    269   */
    270  async _convertEntries(entries, bookmarkURLToUUIDMap) {
    271    let favicons = [];
    272    let convertedEntries = [];
    273 
    274    const faviconFolder = FileUtils.getDir("ULibDir", [
    275      "Safari",
    276      "Favicon Cache",
    277    ]).path;
    278 
    279    for (const entry of entries) {
    280      let type = entry.get("WebBookmarkType");
    281      if (type == "WebBookmarkTypeList" && entry.has("Children")) {
    282        let convertedChildren = await this._convertEntries(
    283          entry.get("Children"),
    284          bookmarkURLToUUIDMap
    285        );
    286        favicons.push(...convertedChildren.favicons);
    287        convertedEntries.push({
    288          title: entry.get("Title"),
    289          type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER,
    290          children: convertedChildren.convertedEntries,
    291        });
    292      } else if (type == "WebBookmarkTypeLeaf" && entry.has("URLString")) {
    293        // Check we understand this URL before adding it:
    294        let url = entry.get("URLString");
    295        if (!URL.canParse(url)) {
    296          console.error(
    297            `Ignoring ${url} when importing from Safari because it is not a valid URL.`
    298          );
    299          continue;
    300        }
    301        let title;
    302        if (entry.has("URIDictionary")) {
    303          title = entry.get("URIDictionary").get("title");
    304        }
    305        convertedEntries.push({ url, title });
    306 
    307        try {
    308          // Try to get the favicon data for each bookmark we have.
    309          // We use uri.spec as our unique identifier since bookmark links
    310          // don't completely match up in the Safari data.
    311          let uri = Services.io.newURI(url);
    312          let uriSpec = uri.spec;
    313 
    314          // Safari's favicon database doesn't include forward slashes for
    315          // the page URLs, despite adding them in the Bookmarks.plist file.
    316          // We'll strip any off here for our favicon lookup.
    317          if (uriSpec.endsWith("/")) {
    318            uriSpec = uriSpec.replace(/\/+$/, "");
    319          }
    320 
    321          let uuid = bookmarkURLToUUIDMap.get(uriSpec);
    322          if (uuid) {
    323            // Hash the UUID with md5 to give us the favicon file name.
    324            let hashedUUID = lazy.PlacesUtils.md5(uuid, {
    325              format: "hex",
    326            }).toUpperCase();
    327            let faviconFile = PathUtils.join(
    328              faviconFolder,
    329              "favicons",
    330              hashedUUID
    331            );
    332            let faviconData = await IOUtils.read(faviconFile);
    333            favicons.push({ faviconData, uri });
    334          }
    335        } catch (error) {
    336          // Even if we fail, still continue the import process
    337          // since favicons aren't as essential as the bookmarks themselves.
    338          console.error(error);
    339        }
    340      }
    341    }
    342 
    343    return { convertedEntries, favicons };
    344  },
    345 };
    346 
    347 async function GetHistoryResource() {
    348  let dbPath = FileUtils.getDir("ULibDir", ["Safari", "History.db"]).path;
    349  let maxAge = msToNSDate(
    350    Date.now() - MigrationUtils.HISTORY_MAX_AGE_IN_MILLISECONDS
    351  );
    352 
    353  // If we have read access to the Safari profile directory, check to
    354  // see if there's any history to import. If we can't access the profile
    355  // directory, let's assume that there's history to import and give the
    356  // user the option to migrate it.
    357  let canReadHistory = false;
    358  try {
    359    // 'stat' is always allowed, but reading is somehow not, if the user hasn't
    360    // allowed it:
    361    await IOUtils.read(dbPath, { maxBytes: 1 });
    362    canReadHistory = true;
    363  } catch (ex) {
    364    console.error(
    365      "Cannot yet read from Safari profile directory. Will presume history exists for import."
    366    );
    367  }
    368 
    369  if (canReadHistory) {
    370    let countQuery = `
    371      SELECT COUNT(*)
    372      FROM history_items LEFT JOIN history_visits
    373      ON history_items.id = history_visits.history_item
    374      WHERE history_visits.visit_time > ${maxAge}
    375      LIMIT 1;`;
    376 
    377    let countResult = await MigrationUtils.getRowsFromDBWithoutLocks(
    378      dbPath,
    379      "Safari history",
    380      countQuery
    381    );
    382 
    383    if (!countResult[0].getResultByName("COUNT(*)")) {
    384      return null;
    385    }
    386  }
    387 
    388  let selectQuery = `
    389    SELECT
    390      history_items.url as history_url,
    391      history_visits.title as history_title,
    392      history_visits.visit_time as history_time
    393    FROM history_items LEFT JOIN history_visits
    394    ON history_items.id = history_visits.history_item
    395    WHERE history_visits.visit_time > ${maxAge};`;
    396 
    397  return {
    398    type: MigrationUtils.resourceTypes.HISTORY,
    399 
    400    async migrate(callback) {
    401      callback(await this._migrate());
    402    },
    403 
    404    async _migrate() {
    405      let historyRows;
    406 
    407      try {
    408        historyRows = await MigrationUtils.getRowsFromDBWithoutLocks(
    409          dbPath,
    410          "Safari history",
    411          selectQuery
    412        );
    413 
    414        if (!historyRows.length) {
    415          console.log("No history found");
    416          return false;
    417        }
    418      } catch (ex) {
    419        console.error(ex);
    420        return false;
    421      }
    422 
    423      let pageInfos = [];
    424      for (let row of historyRows) {
    425        try {
    426          pageInfos.push({
    427            title: row.getResultByName("history_title"),
    428            url: new URL(row.getResultByName("history_url")),
    429            visits: [
    430              {
    431                transition: lazy.PlacesUtils.history.TRANSITIONS.TYPED,
    432                date: parseNSDate(row.getResultByName("history_time")),
    433              },
    434            ],
    435          });
    436        } catch (e) {
    437          console.error("Could not create a history row: ", e);
    438        }
    439      }
    440      await MigrationUtils.insertVisitsWrapper(pageInfos);
    441 
    442      return true;
    443    },
    444  };
    445 }
    446 
    447 /**
    448 * Safari's preferences property list is independently used for three purposes:
    449 * (a) importation of preferences
    450 * (b) importation of search strings
    451 * (c) retrieving the home page.
    452 *
    453 * So, rather than reading it three times, it's cached and managed here.
    454 *
    455 * @param {nsIFile} aPreferencesFile
    456 *   The .plist file to be read.
    457 */
    458 function MainPreferencesPropertyList(aPreferencesFile) {
    459  this._file = aPreferencesFile;
    460  this._callbacks = [];
    461 }
    462 MainPreferencesPropertyList.prototype = {
    463  /**
    464   * @see PropertyListUtils.read
    465   * @param {Function} aCallback
    466   *   A callback called with an Object representing the key-value pairs
    467   *   read out of the .plist file.
    468   */
    469  read: function MPPL_read(aCallback) {
    470    if ("_dict" in this) {
    471      aCallback(this._dict);
    472      return;
    473    }
    474 
    475    let alreadyReading = !!this._callbacks.length;
    476    this._callbacks.push(aCallback);
    477    if (!alreadyReading) {
    478      lazy.PropertyListUtils.read(this._file, aDict => {
    479        this._dict = aDict;
    480        for (let callback of this._callbacks) {
    481          try {
    482            callback(aDict);
    483          } catch (ex) {
    484            console.error(ex);
    485          }
    486        }
    487        this._callbacks.splice(0);
    488      });
    489    }
    490  },
    491 };
    492 
    493 function SearchStrings(aMainPreferencesPropertyListInstance) {
    494  this._mainPreferencesPropertyList = aMainPreferencesPropertyListInstance;
    495 }
    496 SearchStrings.prototype = {
    497  type: MigrationUtils.resourceTypes.OTHERDATA,
    498 
    499  migrate: function SS_migrate(aCallback) {
    500    this._mainPreferencesPropertyList.read(
    501      MigrationUtils.wrapMigrateFunction(function migrateSearchStrings(aDict) {
    502        if (!aDict) {
    503          throw new Error("Could not get preferences dictionary");
    504        }
    505 
    506        if (aDict.has("RecentSearchStrings")) {
    507          let recentSearchStrings = aDict.get("RecentSearchStrings");
    508          if (recentSearchStrings && recentSearchStrings.length) {
    509            let changes = recentSearchStrings.map(searchString => ({
    510              op: "add",
    511              fieldname: "searchbar-history",
    512              value: searchString,
    513            }));
    514            lazy.FormHistory.update(changes);
    515          }
    516        }
    517      }, aCallback)
    518    );
    519  },
    520 };
    521 
    522 /**
    523 * Safari migrator
    524 */
    525 export class SafariProfileMigrator extends MigratorBase {
    526  static get key() {
    527    return "safari";
    528  }
    529 
    530  static get displayNameL10nID() {
    531    return "migration-wizard-migrator-display-name-safari";
    532  }
    533 
    534  static get brandImage() {
    535    return "chrome://browser/content/migration/brands/safari.png";
    536  }
    537 
    538  async getResources() {
    539    let profileDir = FileUtils.getDir("ULibDir", ["Safari"]);
    540    if (!profileDir.exists()) {
    541      return null;
    542    }
    543 
    544    let resources = [];
    545    let pushProfileFileResource = function (aFileName, aConstructor) {
    546      let file = profileDir.clone();
    547      file.append(aFileName);
    548      if (file.exists()) {
    549        resources.push(new aConstructor(file));
    550      }
    551    };
    552 
    553    pushProfileFileResource("Bookmarks.plist", Bookmarks);
    554 
    555    // The Reading List feature was introduced at the same time in Windows and
    556    // Mac versions of Safari.  Not surprisingly, they are stored in the same
    557    // format in both versions.  Surpsingly, only on Windows there is a
    558    // separate property list for it.  This code is used on mac too, because
    559    // Apple may fix this at some point.
    560    pushProfileFileResource("ReadingList.plist", Bookmarks);
    561 
    562    let prefs = this.mainPreferencesPropertyList;
    563    if (prefs) {
    564      resources.push(new SearchStrings(prefs));
    565    }
    566 
    567    resources.push(GetHistoryResource());
    568 
    569    resources = await Promise.all(resources);
    570 
    571    return resources.filter(r => r != null);
    572  }
    573 
    574  async getLastUsedDate() {
    575    const profileDir = FileUtils.getDir("ULibDir", ["Safari"]);
    576    const dates = await Promise.all(
    577      ["Bookmarks.plist", "History.db"].map(file => {
    578        const path = PathUtils.join(profileDir.path, file);
    579        return IOUtils.stat(path)
    580          .then(info => info.lastModified)
    581          .catch(() => 0);
    582      })
    583    );
    584 
    585    return new Date(Math.max(...dates));
    586  }
    587 
    588  async hasPermissions() {
    589    if (this._hasPermissions) {
    590      return true;
    591    }
    592    // Check if we have access to some key files, but only if they exist.
    593    let historyTarget = FileUtils.getDir("ULibDir", ["Safari", "History.db"]);
    594    let bookmarkTarget = FileUtils.getDir("ULibDir", [
    595      "Safari",
    596      "Bookmarks.plist",
    597    ]);
    598    let faviconTarget = FileUtils.getDir("ULibDir", [
    599      "Safari",
    600      "Favicon Cache",
    601      "favicons.db",
    602    ]);
    603    try {
    604      let historyExists = await IOUtils.exists(historyTarget.path);
    605      let bookmarksExists = await IOUtils.exists(bookmarkTarget.path);
    606      let faviconsExists = await IOUtils.exists(faviconTarget.path);
    607      // We now know which files exist, which is always allowed.
    608      // To determine if we have read permissions, try to read a single byte
    609      // from each file that exists, which will throw if we need permissions.
    610      if (historyExists) {
    611        await IOUtils.read(historyTarget.path, { maxBytes: 1 });
    612      }
    613      if (bookmarksExists) {
    614        await IOUtils.read(bookmarkTarget.path, { maxBytes: 1 });
    615      }
    616      if (faviconsExists) {
    617        await IOUtils.read(faviconTarget.path, { maxBytes: 1 });
    618      }
    619      this._hasPermissions = true;
    620      return true;
    621    } catch (ex) {
    622      return false;
    623    }
    624  }
    625 
    626  async getPermissions(win) {
    627    // Keep prompting the user until they pick something that grants us access
    628    // to Safari's bookmarks and favicons or they cancel out of the file open panel.
    629    while (!(await this.hasPermissions())) {
    630      let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    631      // The title (second arg) is not displayed on macOS, so leave it blank.
    632      fp.init(win?.browsingContext, "", Ci.nsIFilePicker.modeGetFolder);
    633      fp.filterIndex = 1;
    634      fp.displayDirectory = FileUtils.getDir("ULibDir", [""]);
    635      // Now wait for the filepicker to open and close. If the user picks
    636      // the Safari folder, macOS will grant us read access to everything
    637      // inside, so we don't need to check or do anything else with what's
    638      // returned by the filepicker.
    639      let result = await new Promise(resolve => fp.open(resolve));
    640      // Bail if the user cancels the dialog:
    641      if (result == Ci.nsIFilePicker.returnCancel) {
    642        return false;
    643      }
    644    }
    645    return true;
    646  }
    647 
    648  async canGetPermissions() {
    649    if (await MigrationUtils.canGetPermissionsOnPlatform()) {
    650      const profileDir = FileUtils.getDir("ULibDir", ["Safari"]);
    651      if (await IOUtils.exists(profileDir.path)) {
    652        return profileDir.path;
    653      }
    654    }
    655    return false;
    656  }
    657 
    658  /**
    659   * For Safari on macOS, we show a specialized flow for importing passwords
    660   * from a CSV file.
    661   *
    662   * @returns {boolean}
    663   */
    664  get showsManualPasswordImport() {
    665    // Since this migrator will only ever be used on macOS, all conditions are
    666    // met and we can always return true.
    667    return true;
    668  }
    669 
    670  get mainPreferencesPropertyList() {
    671    if (this._mainPreferencesPropertyList === undefined) {
    672      let file = FileUtils.getDir("UsrPrfs", []);
    673      if (file.exists()) {
    674        file.append("com.apple.Safari.plist");
    675        if (file.exists()) {
    676          this._mainPreferencesPropertyList = new MainPreferencesPropertyList(
    677            file
    678          );
    679          return this._mainPreferencesPropertyList;
    680        }
    681      }
    682      this._mainPreferencesPropertyList = null;
    683      return this._mainPreferencesPropertyList;
    684    }
    685    return this._mainPreferencesPropertyList;
    686  }
    687 }