/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

// Tests the semantics of extension proxy files and symlinks

var ADDONS = [
  {
    id: "proxy1@tests.mozilla.org",
    dirId: "proxy1@tests.mozilla.com",
    type: "proxy",
  },
  {
    id: "proxy2@tests.mozilla.org",
    type: "proxy",
  },
  {
    id: "symlink1@tests.mozilla.org",
    dirId: "symlink1@tests.mozilla.com",
    type: "symlink",
  },
  {
    id: "symlink2@tests.mozilla.org",
    type: "symlink",
  },
];

const gHaveSymlinks = AppConstants.platform != "win";

function createSymlink(aSource, aDest) {
  if (aSource instanceof Ci.nsIFile) {
    aSource = aSource.path;
  }
  if (aDest instanceof Ci.nsIFile) {
    aDest = aDest.path;
  }

  const ln = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
  ln.initWithPath("/bin/ln");

  const lnProcess = Cc["@mozilla.org/process/util;1"].createInstance(
    Ci.nsIProcess
  );
  lnProcess.init(ln);

  const args = ["-s", aSource, aDest];
  lnProcess.run(true, args, args.length);
  Assert.equal(lnProcess.exitValue, 0);
}

async function promiseWriteFile(aFile, aData) {
  if (!(await IOUtils.exists(aFile.parent.path))) {
    await IOUtils.makeDirectory(aFile.parent.path);
  }

  return IOUtils.writeUTF8(aFile.path, aData);
}

function checkAddonsExist() {
  for (let addon of ADDONS) {
    let file = addon.directory.clone();
    file.append("manifest.json");
    Assert.ok(file.exists());
  }
}

const profileDir = gProfD.clone();
profileDir.append("extensions");

function run_test() {
  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");

  // Unpacked extensions are never signed, so this can only run with
  // signature checks disabled.
  Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, false);

  add_task(run_proxy_tests);

  if (gHaveSymlinks) {
    add_task(run_symlink_tests);
  }

  run_next_test();
}

async function run_proxy_tests() {
  if (!gHaveSymlinks) {
    ADDONS = ADDONS.filter(a => a.type != "symlink");
  }

  for (let addon of ADDONS) {
    addon.directory = gTmpD.clone();
    addon.directory.append(addon.id);

    addon.proxyFile = profileDir.clone();
    addon.proxyFile.append(addon.dirId || addon.id);

    let files = ExtensionTestCommon.generateFiles({
      manifest: {
        name: addon.id,
        browser_specific_settings: { gecko: { id: addon.id } },
      },
    });
    let path = PathUtils.join(gTmpD.path, addon.id);
    await AddonTestUtils.promiseWriteFilesToDir(path, files);

    if (addon.type == "proxy") {
      await promiseWriteFile(addon.proxyFile, addon.directory.path);
    } else if (addon.type == "symlink") {
      await createSymlink(addon.directory, addon.proxyFile);
    }
  }

  await promiseStartupManager();

  // Check that all add-ons original sources still exist after invalid
  // add-ons have been removed at startup.
  checkAddonsExist();

  let addons = await AddonManager.getAddonsByIDs(ADDONS.map(addon => addon.id));
  try {
    for (let [i, addon] of addons.entries()) {
      // Ensure that valid proxied add-ons were installed properly on
      // platforms that support the installation method.
      print(
        ADDONS[i].id,
        ADDONS[i].dirId,
        ADDONS[i].dirId != null,
        ADDONS[i].type == "symlink"
      );
      Assert.equal(addon == null, ADDONS[i].dirId != null);

      if (addon != null) {
        let fixURL = url => {
          if (AppConstants.platform == "macosx") {
            return url.replace(RegExp(`^file:///private/`), "file:///");
          }
          return url;
        };

        // Check that proxied add-ons do not have upgrade permissions.
        Assert.equal(addon.permissions & AddonManager.PERM_CAN_UPGRADE, 0);

        // Check that getResourceURI points to the right place.
        Assert.equal(
          Services.io.newFileURI(ADDONS[i].directory).spec,
          fixURL(addon.getResourceURI().spec),
          `Base resource URL resolves as expected`
        );

        let file = ADDONS[i].directory.clone();
        file.append("manifest.json");

        Assert.equal(
          Services.io.newFileURI(file).spec,
          fixURL(addon.getResourceURI("manifest.json").spec),
          `Resource URLs resolve as expected`
        );

        await addon.uninstall();
      }
    }

    // Check that original sources still exist after explicit uninstall.
    await promiseRestartManager();
    checkAddonsExist();

    await promiseShutdownManager();

    // Check that all of the proxy files have been removed and remove
    // the original targets.
    for (let addon of ADDONS) {
      equal(
        addon.proxyFile.exists(),
        addon.dirId != null,
        `Proxy file ${addon.proxyFile.path} should exist?`
      );
      addon.directory.remove(true);
      try {
        addon.proxyFile.remove(false);
      } catch (e) {}
    }
  } catch (e) {
    do_throw(e);
  }
}

// Check that symlinks are not followed out of a directory tree
// when deleting an add-on.
async function run_symlink_tests() {
  const ID = "unpacked@test.mozilla.org";

  let tempDirectory = gTmpD.clone();
  tempDirectory.append(ID);

  let tempFile = tempDirectory.clone();
  tempFile.append("test.txt");
  tempFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);

  let addonDirectory = profileDir.clone();
  addonDirectory.append(ID);

  let files = ExtensionTestCommon.generateFiles({
    manifest: { browser_specific_settings: { gecko: { id: ID } } },
  });
  await AddonTestUtils.promiseWriteFilesToDir(addonDirectory.path, files);

  let symlink = addonDirectory.clone();
  symlink.append(tempDirectory.leafName);
  await createSymlink(tempDirectory, symlink);

  // Make sure that the symlink was created properly.
  let file = symlink.clone();
  file.append(tempFile.leafName);
  file.normalize();
  Assert.equal(file.path.replace(/^\/private\//, "/"), tempFile.path);

  await promiseStartupManager();

  let addon = await AddonManager.getAddonByID(ID);
  Assert.notEqual(addon, null);

  await addon.uninstall();

  await promiseRestartManager();
  await promiseShutdownManager();

  // Check that the install directory is gone.
  Assert.ok(!addonDirectory.exists());

  // Check that the temp file is not gone.
  Assert.ok(tempFile.exists());

  tempDirectory.remove(true);
}
