Пример #1
0
TEST_F(CommandsTest, CreateShortcutEntry)
{
  CItemData bi, si;
  bi.CreateUUID();
  bi.SetTitle(L"base entry");
  bi.SetPassword(L"base password");
  const pws_os::CUUID base_uuid = bi.GetUUID();

  si.SetTitle(L"shortcut to base");
  si.SetPassword(L"[Shortcut]");
  si.SetShortcut();
  si.CreateUUID(); // call after setting to shortcut!

  time_t t;
  time(&t);
  si.SetCTime(t);
  si.SetXTime((time_t)0);
  si.SetStatus(CItemData::ES_ADDED);

  MultiCommands *pmulticmds = MultiCommands::Create(&core);
  pmulticmds->Add(AddEntryCommand::Create(&core, bi));
  pmulticmds->Add(AddEntryCommand::Create(&core, si, base_uuid));
  core.Execute(pmulticmds);
  EXPECT_EQ(2, core.GetNumEntries());

  // Check that the base entry is correctly marked
  ItemListConstIter iter = core.Find(base_uuid);
  ASSERT_NE(core.GetEntryEndIter(), iter);
  EXPECT_TRUE(core.GetEntry(iter).IsShortcutBase());

  core.Undo();
  EXPECT_EQ(0, core.GetNumEntries());

  core.Redo();
  EXPECT_EQ(2, core.GetNumEntries());

  // Delete base, expect both to be gone
  // Get base from core for correct type
  const CItemData bi2 = core.GetEntry(core.Find(base_uuid));
  DeleteEntryCommand *pcmd1 = DeleteEntryCommand::Create(&core, bi2);

  core.Execute(pcmd1);
  EXPECT_EQ(0, core.GetNumEntries());

  core.Undo();
  EXPECT_EQ(2, core.GetNumEntries());

  // Now just delete the shortcut, check that
  // base is left, and that it reverts to a normal entry
  const CItemData si2 = core.GetEntry(core.Find(si.GetUUID())); // si2 has baseUUID set
  DeleteEntryCommand *pcmd2 = DeleteEntryCommand::Create(&core, si2);

  core.Execute(pcmd2);
  ASSERT_EQ(1, core.GetNumEntries());
  EXPECT_TRUE(core.GetEntry(core.Find(base_uuid)).IsNormal());

  // Get core to delete any existing commands
  core.ClearCommands();
}
Пример #2
0
TEST_F(AliasShortcutTest, Alias)
{
  CItemData al;

  al.CreateUUID();
  al.SetTitle(L"alias");
  al.SetPassword(L"alias-password-not-used");
  al.SetUser(L"alias-user");
  al.SetNotes(L"alias-notes");
  al.SetGroup(L"Galias");
  al.SetURL(L"http://alias-url.com");
  al.SetAutoType(L"alias-autotype");
  al.SetEmail(L"*****@*****.**");
  al.SetRunCommand(L"Run alias, run");
  al.SetAlias();

  const pws_os::CUUID base_uuid = base.GetUUID();
  MultiCommands *pmulticmds = MultiCommands::Create(&core);
  pmulticmds->Add(AddEntryCommand::Create(&core, base));
  pmulticmds->Add(AddEntryCommand::Create(&core, al, base_uuid));
  core.Execute(pmulticmds);
  EXPECT_EQ(2, core.GetNumEntries());

  const CItemData al2 = core.GetEntry(core.Find(al.GetUUID()));

  StringX sx_group, sx_title, sx_user, sx_pswd, sx_lastpswd, sx_notes, sx_url, sx_email, sx_autotype, sx_runcmd;

  bool status = PWSAuxParse::GetEffectiveValues(&al2, &base,
                                               sx_group, sx_title, sx_user, sx_pswd, sx_lastpswd, sx_notes,
                                               sx_url, sx_email, sx_autotype, sx_runcmd);
  EXPECT_TRUE(status);
  // Password should be from base:
  EXPECT_EQ(sx_pswd, base.GetPassword());
  // All the rest should be from alias:
  EXPECT_EQ(sx_group, al.GetGroup());
  EXPECT_EQ(sx_title, al.GetTitle());
  EXPECT_EQ(sx_user, al.GetUser());
  EXPECT_EQ(sx_lastpswd, L"");
  EXPECT_EQ(sx_notes, al.GetNotes());
  EXPECT_EQ(sx_url, al.GetURL());
  EXPECT_EQ(sx_email, al.GetEmail());
  EXPECT_EQ(sx_autotype, al.GetAutoType());
  EXPECT_EQ(sx_runcmd, al.GetRunCommand());
}
void PWSTreeCtrl::FinishRenamingGroup(wxTreeEvent& evt, wxTreeItemId groupItem, const wxString& oldPath)
{
  wxCHECK_RET(ItemIsGroup(groupItem), wxT("Cannot handle renaming of non-group items"));

  if (evt.IsEditCancelled())
    return;

  // We DON'T need to handle these two as they can only occur while moving items
  //    not removing groups as they become empty
  //    renaming of groups that have only other groups as children

  MultiCommands* pmcmd = MultiCommands::Create(&m_core);
  if (!pmcmd)
    return;

  // For some reason, Command objects can't handle const references
  StringX sxOldPath = tostringx(oldPath);
  StringX sxNewPath = tostringx(GetItemGroup(groupItem));

  // This takes care of modifying all the actual items
  pmcmd->Add(RenameGroupCommand::Create(&m_core, sxOldPath, sxNewPath));

  // But we have to do the empty groups ourselves because EG_RENAME is not recursive
  typedef std::vector<StringX> EmptyGroupsArray;
  const EmptyGroupsArray& emptyGroups = m_core.GetEmptyGroups();
  StringX sxOldPathWithDot = sxOldPath + _T('.');
  for( EmptyGroupsArray::const_iterator itr = emptyGroups.begin(); itr != emptyGroups.end(); ++itr)
  {
    if (*itr == sxOldPath || itr->find(sxOldPathWithDot) == 0) {
      StringX sxOld = *itr;
      StringX sxNew = sxNewPath + itr->substr(sxOldPath.size());
      pmcmd->Add(DBEmptyGroupsCommand::Create(&m_core, sxOld, sxNew));
    }
  }

  if (pmcmd->GetSize())
    m_core.Execute(pmcmd);

  // The old treeItem is gone, since it was renamed.  We need to find the new one to select it
  wxTreeItemId newItem = Find(towxstring(sxNewPath), GetRootItem());
  if (newItem.IsOk())
    wxTreeCtrl::SelectItem(newItem);
}
Пример #4
0
TEST_F(AliasShortcutTest, Shortcut)
{
  CItemData sc;

  sc.SetTitle(L"shortcut");
  sc.SetUser(L"sc-user");
  sc.SetGroup(L"sc-group");
  sc.SetPassword(L"[Shortcut]");
  sc.SetShortcut();
  sc.CreateUUID(); // call after setting to shortcut!

  const pws_os::CUUID base_uuid = base.GetUUID();
  MultiCommands *pmulticmds = MultiCommands::Create(&core);
  pmulticmds->Add(AddEntryCommand::Create(&core, base));
  pmulticmds->Add(AddEntryCommand::Create(&core, sc, base_uuid));
  core.Execute(pmulticmds);
  EXPECT_EQ(2, core.GetNumEntries());

  const CItemData sc2 = core.GetEntry(core.Find(sc.GetUUID()));

  StringX sx_group, sx_title, sx_user, sx_pswd, sx_lastpswd, sx_notes, sx_url, sx_email, sx_autotype, sx_runcmd;

  bool status = PWSAuxParse::GetEffectiveValues(&sc2, &base,
                                               sx_group, sx_title, sx_user, sx_pswd, sx_lastpswd, sx_notes,
                                               sx_url, sx_email, sx_autotype, sx_runcmd);
  EXPECT_TRUE(status);
  // Group, title and user should all be from sc:
  EXPECT_EQ(sx_group, sc.GetGroup());
  EXPECT_EQ(sx_title, sc.GetTitle());
  EXPECT_EQ(sx_user, sc.GetUser());
  // All the rest should be from base:
  EXPECT_EQ(sx_pswd, base.GetPassword());
  EXPECT_EQ(sx_lastpswd, L"");
  EXPECT_EQ(sx_notes, base.GetNotes());
  EXPECT_EQ(sx_url, base.GetURL());
  EXPECT_EQ(sx_email, base.GetEmail());
  EXPECT_EQ(sx_autotype, base.GetAutoType());
  EXPECT_EQ(sx_runcmd, base.GetRunCommand());
}
Пример #5
0
void CManagePasswordPolicies::OnOkClick( wxCommandEvent& )
{
  /*
   * User may have changed default policy, named policy, none or both.
   * If anything has changed, we treat the change as atomic, creating a multicommand
   * s.t. Undo/Redo will work as expected.
   */
  PWPolicy olddefpol(PWSprefs::GetInstance()->GetDefaultPolicy());
  bool defChanged = (olddefpol != m_st_default_pp);
  bool namedChanged = (m_MapPSWDPLC != m_core.GetPasswordPolicies());

  if (defChanged || namedChanged) {
    MultiCommands *pmulticmds = MultiCommands::Create(&m_core);

    if (defChanged) {
      // User has changed database default policy - need to update preferences
      // Update the copy only!
      PWSprefs::GetInstance()->SetupCopyPrefs();
      PWSprefs::GetInstance()->SetDefaultPolicy(m_st_default_pp, true);

      // Now get new DB preferences String value
      StringX sxNewDBPrefsString(PWSprefs::GetInstance()->Store(true));

      // Set up Command to update string in database
      if (m_core.GetReadFileVersion() == PWSfile::VCURRENT)
        pmulticmds->Add(DBPrefsCommand::Create(&m_core, sxNewDBPrefsString));
    } // defChanged

    if (namedChanged) {
      pmulticmds->Add(DBPolicyNamesCommand::Create(&m_core, m_MapPSWDPLC,
                                                   DBPolicyNamesCommand::NP_REPLACEALL));
    }
    m_core.Execute(pmulticmds);
  } // defChanged || namedChanged
  EndModal(wxID_OK);
}
Command *PasswordSafeFrame::Delete(wxTreeItemId tid)
{
  // Called for deleting a group
  // Recursively build the appropriate multi-command

  if (!tid) return NULL;
  MultiCommands *retval = MultiCommands::Create(&m_core);
  if (m_tree->GetChildrenCount(tid) > 0) {
    wxTreeItemIdValue cookie;
    wxTreeItemId ti = m_tree->GetFirstChild(tid, cookie);

    while (ti.IsOk()) {
      Command *delCmd = Delete(ti);
      if (delCmd != NULL)
        retval->Add(delCmd);
      ti = m_tree->GetNextChild(tid, cookie);
    } // while children
    // Explicitly delete any empty groups coinciding with this wxTreeItem's group hierarchy
    // Otherwise the user will see a these empty groups still hanging around inspite
    // of just deleting the parent/ancestor
    StringX sxGroup = tostringx(m_tree->GetItemGroup(tid));
    if (m_core.IsEmptyGroup(sxGroup)) {
      Command *delGrp = DBEmptyGroupsCommand::Create(&m_core, sxGroup, DBEmptyGroupsCommand::EG_DELETE);
      if (delGrp)
        retval->Add(delGrp);
    }
  } else { // leaf
    CItemData *leaf = m_tree->GetItem(tid);
    if (leaf != NULL) {
      Command *delLeafCmd = Delete(leaf); // gets user conf. if needed
      if (delLeafCmd != NULL)
        retval->Add(delLeafCmd);
    }
    else {
      wxASSERT_MSG(m_tree->ItemIsGroup(tid), wxT("Childless item without CItemData must be an empty group"));
      StringX sxGroup = tostringx(m_tree->GetItemGroup(tid));
      Command *delGrp = DBEmptyGroupsCommand::Create(&m_core, sxGroup, DBEmptyGroupsCommand::EG_DELETE);
      if (delGrp)
        retval->Add(delGrp);
    }
  }

  // If MultiCommands is empty, delete and return NULL
  if (retval->GetSize() == 0) {
    delete retval;
    retval = NULL;
  }
  return retval;
}
void PWScore::Synchronize(PWScore *pothercore,
                          const CItemData::FieldBits &bsFields, const bool &subgroup_bset,
                          const stringT &subgroup_name,
                          const int &subgroup_object, const int &subgroup_function,
                          int &numUpdated, CReport *pRpt, bool *pbCancel)
{
  /*
  Purpose:
    Synchronize entries from otherCore to m_core

  Algorithm:
    Foreach entry in otherCore
      Find in m_core
        if find a match
          update requested fields
  */

  std::vector<StringX> vs_updated;
  numUpdated = 0;

  MultiCommands *pmulticmds = MultiCommands::Create(this);
  Command *pcmd1 = UpdateGUICommand::Create(this, UpdateGUICommand::WN_UNDO,
                                            UpdateGUICommand::GUI_UNDO_MERGESYNC);
  pmulticmds->Add(pcmd1);

  // Make sure we don't add it multiple times
  std::map<StringX, StringX> mapRenamedPolicies;
  std::vector<StringX> vs_PoliciesAdded;
  const StringX sxSync_DateTime = PWSUtil::GetTimeStamp(true).c_str();

  ItemListConstIter otherPos;
  for (otherPos = pothercore->GetEntryIter();
       otherPos != pothercore->GetEntryEndIter();
       otherPos++) {
    // See if user has cancelled
    if (pbCancel != NULL && *pbCancel) {
      delete pmulticmds;
      return;
    }

    CItemData otherItem = pothercore->GetEntry(otherPos);
    CItemData::EntryType et = otherItem.GetEntryType();

    // Do not process Aliases and Shortcuts
    if (et == CItemData::ET_ALIAS || et == CItemData::ET_SHORTCUT)
      continue;

    if (subgroup_bset &&
        !otherItem.Matches(subgroup_name, subgroup_object, subgroup_function))
      continue;

    const StringX sx_otherGroup = otherItem.GetGroup();
    const StringX sx_otherTitle = otherItem.GetTitle();
    const StringX sx_otherUser = otherItem.GetUser();

    StringX sx_mergedentry;
    Format(sx_mergedentry, GROUPTITLEUSERINCHEVRONS,
                sx_otherGroup.c_str(), sx_otherTitle.c_str(), sx_otherUser.c_str());

    ItemListConstIter foundPos = Find(sx_otherGroup, sx_otherTitle, sx_otherUser);

    if (foundPos != GetEntryEndIter()) {
      // found a match
      CItemData curItem = GetEntry(foundPos);

      // Don't update if entry is protected
      if (curItem.IsProtected())
        continue;

      CItemData updItem(curItem);
      updItem.SetDisplayInfo(NULL);

      if (curItem.GetUUID() != otherItem.GetUUID()) {
        pws_os::Trace(_T("Synchronize: Mis-match UUIDs for [%ls:%ls:%ls]\n"),
             sx_otherGroup.c_str(), sx_otherTitle.c_str(), sx_otherUser.c_str());
      }

      bool bUpdated(false);
      // Do not try and change GROUPTITLE = 0x00 (use GROUP & TITLE separately) or UUID = 0x01
      for (size_t i = 2; i < bsFields.size(); i++) {
        if (bsFields.test(i)) {
          StringX sxValue = otherItem.GetFieldValue(static_cast<CItemData::FieldType>(i));

          // Special processing for password policies (default & named)
          if (static_cast<CItemData::FieldType>(i) == CItemData::POLICYNAME) {
            Command *pPolicyCmd = ProcessPolicyName(pothercore, updItem,
                                   mapRenamedPolicies, vs_PoliciesAdded,
                                   sxValue, bUpdated,
                                   sxSync_DateTime, IDSC_SYNCPOLICY);
            if (pPolicyCmd != NULL)
              pmulticmds->Add(pPolicyCmd);
          } else {
            if (sxValue != updItem.GetFieldValue(static_cast<CItemData::FieldType>(i))) {
              bUpdated = true;
              updItem.SetFieldValue(static_cast<CItemData::FieldType>(i), sxValue);
            }
          }
        }
      }

      if (!bUpdated)
        continue;

      GUISetupDisplayInfo(updItem);
      updItem.SetStatus(CItemData::ES_MODIFIED);

      StringX sx_updated;
      Format(sx_updated, GROUPTITLEUSERINCHEVRONS,
                sx_otherGroup.c_str(), sx_otherTitle.c_str(), sx_otherUser.c_str());
      vs_updated.push_back(sx_updated);

      Command *pcmd = EditEntryCommand::Create(this, curItem, updItem);
      pcmd->SetNoGUINotify();
      pmulticmds->Add(pcmd);

      // Update the Wizard page
      UpdateWizard(sx_updated.c_str());

      numUpdated++;
    }  // Found match via [g:t:u]
  } // iteration over other core's entries

  stringT str_results;
  if (numUpdated > 0 && pRpt != NULL) {
    std::sort(vs_updated.begin(), vs_updated.end(), MergeSyncGTUCompare);
    stringT str_singular_plural_type, str_singular_plural_verb;
    LoadAString(str_singular_plural_type, numUpdated == 1 ? IDSC_ENTRY : IDSC_ENTRIES);
    LoadAString(str_singular_plural_verb, numUpdated == 1 ? IDSC_WAS : IDSC_WERE);
    Format(str_results, IDSC_SYNCHUPDATED, str_singular_plural_type.c_str(),
                    str_singular_plural_verb.c_str());
    pRpt->WriteLine(str_results.c_str());
    for (size_t i = 0; i < vs_updated.size(); i++) {
      Format(str_results, L"\t%ls", vs_updated[i].c_str());
      pRpt->WriteLine(str_results.c_str());
    }
  }

  // See if user has cancelled
  if (pbCancel != NULL && *pbCancel) {
    delete pmulticmds;
    return;
  }

  Command *pcmd2 = UpdateGUICommand::Create(this, UpdateGUICommand::WN_REDO,
                                            UpdateGUICommand::GUI_REDO_MERGESYNC);
  pmulticmds->Add(pcmd2);
  Execute(pmulticmds);

  // See if user has cancelled too late - reset flag so incorrect information not given to user
  if (pbCancel != NULL && *pbCancel) {
    *pbCancel = false;
    return;
  }

  // tell the user we're done & provide short Synchronize report
  stringT str_entries;
  LoadAString(str_entries, numUpdated == 1 ? IDSC_ENTRY : IDSC_ENTRIES);
  Format(str_results, IDSC_SYNCHCOMPLETED, numUpdated, str_entries.c_str());
  pRpt->WriteLine(str_results.c_str());
}
stringT PWScore::Merge(PWScore *pothercore,
                       const bool &subgroup_bset,
                       const stringT &subgroup_name,
                       const int &subgroup_object, const int &subgroup_function,
                       CReport *pRpt, bool *pbCancel)
{
  std::vector<StringX> vs_added;
  std::vector<StringX> vs_AliasesAdded;
  std::vector<StringX> vs_ShortcutsAdded;
  std::vector<StringX> vs_PoliciesAdded;
  std::map<StringX, StringX> mapRenamedPolicies;

  const StringX sxMerge_DateTime = PWSUtil::GetTimeStamp(true).c_str();

  stringT str_timestring;  // To append to title if already in current database
  str_timestring = sxMerge_DateTime.c_str();
  Remove(str_timestring, _T('/'));
  Remove(str_timestring, _T(':'));

  /*
    Purpose:
      Merge entries from otherCore to m_core

    Algorithm:
      Foreach entry in otherCore
        Find in m_core based on group/title/username
          if match found {
            if all other fields match {
              no merge
            } else {
              add to m_core with new title suffixed with -merged-YYYYMMDD-HHMMSS
            }
          } else {
            add to m_core directly
          }
   */

  int numAdded = 0;
  int numConflicts = 0;
  int numAliasesAdded = 0;
  int numShortcutsAdded = 0;
  uuid_array_t base_uuid, new_base_uuid;
  bool bTitleRenamed(false);
  StringX sx_merged;
  LoadAString(sx_merged, IDSC_MERGED);

  MultiCommands *pmulticmds = MultiCommands::Create(this);
  Command *pcmd1 = UpdateGUICommand::Create(this, UpdateGUICommand::WN_UNDO,
                                            UpdateGUICommand::GUI_UNDO_MERGESYNC);
  pmulticmds->Add(pcmd1);

  ItemListConstIter otherPos;
  for (otherPos = pothercore->GetEntryIter();
       otherPos != pothercore->GetEntryEndIter();
       otherPos++) {
    // See if user has cancelled
    if (pbCancel != NULL && *pbCancel) {
      delete pmulticmds;
      return _T("");
    }

    CItemData otherItem = pothercore->GetEntry(otherPos);
    CItemData::EntryType et = otherItem.GetEntryType();

    // Need to check that entry keyboard shortcut not already in use!
    int32 iKBShortcut;
    otherItem.GetKBShortcut(iKBShortcut);
    CUUID kbshortcut_uuid = GetKBShortcut(iKBShortcut);
    bool bKBShortcutInUse = (iKBShortcut != 0&& kbshortcut_uuid != CUUID::NullUUID());

    // Handle Aliases and Shortcuts when processing their base entries
    if (otherItem.IsDependent())
      continue;

    if (subgroup_bset &&
        !otherItem.Matches(subgroup_name, subgroup_object, subgroup_function))
      continue;

    const StringX sx_otherGroup = otherItem.GetGroup();
    const StringX sx_otherTitle = otherItem.GetTitle();
    const StringX sx_otherUser = otherItem.GetUser();

    StringX sxMergedEntry;
    Format(sxMergedEntry, GROUPTITLEUSERINCHEVRONS,
                sx_otherGroup.c_str(), sx_otherTitle.c_str(), sx_otherUser.c_str());

    ItemListConstIter foundPos = Find(sx_otherGroup, sx_otherTitle, sx_otherUser);

    otherItem.GetUUID(base_uuid);
    memcpy(new_base_uuid, base_uuid, sizeof(new_base_uuid));
    bTitleRenamed = false;

    if (foundPos != GetEntryEndIter()) {
      // Found a match, see if other fields also match
      CItemData curItem = GetEntry(foundPos);

      // Can't merge into a protected entry.  If we were going to - add instead
      unsigned char ucprotected;
      curItem.GetProtected(ucprotected);

      stringT str_diffs(_T("")), str_temp;
      int diff_flags = 0;
      int32 cxtint, oxtint;
      time_t cxt, oxt;
      if (otherItem.GetPassword() != curItem.GetPassword()) {
        diff_flags |= MRG_PASSWORD;
        LoadAString(str_temp, IDSC_FLDNMPASSWORD);
        str_diffs += str_temp + _T(", ");
      }

      if (otherItem.GetNotes() != curItem.GetNotes()) {
        diff_flags |= MRG_NOTES;
        LoadAString(str_temp, IDSC_FLDNMNOTES);
        str_diffs += str_temp + _T(", ");
      }

      if (otherItem.GetURL() != curItem.GetURL()) {
        diff_flags |= MRG_URL;
        LoadAString(str_temp, IDSC_FLDNMURL);
        str_diffs += str_temp + _T(", ");
      }

      if (otherItem.GetAutoType() != curItem.GetAutoType()) {
        diff_flags |= MRG_AUTOTYPE;
        LoadAString(str_temp, IDSC_FLDNMAUTOTYPE);
        str_diffs += str_temp + _T(", ");
      }

      if (otherItem.GetPWHistory() != curItem.GetPWHistory()) {
        diff_flags |= MRG_HISTORY;
        LoadAString(str_temp, IDSC_FLDNMPWHISTORY);
        str_diffs += str_temp + _T(", ");
      }

      // Don't test policy or symbols if either entry is using a named policy
      // as these are meaningless to compare
      if (otherItem.GetPolicyName().empty() && curItem.GetPolicyName().empty()) {
        PWPolicy cur_pwp, oth_pwp;
        if (curItem.GetPWPolicy().empty())
          cur_pwp = PWSprefs::GetInstance()->GetDefaultPolicy();
        else
          curItem.GetPWPolicy(cur_pwp);
        if (otherItem.GetPWPolicy().empty())
          oth_pwp = PWSprefs::GetInstance()->GetDefaultPolicy(true);
        else
          otherItem.GetPWPolicy(oth_pwp);
        if (cur_pwp != oth_pwp) {
          diff_flags |= MRG_POLICY;
          LoadAString(str_temp, IDSC_FLDNMPWPOLICY);
          str_diffs += str_temp + _T(", ");
        }
      }

      otherItem.GetXTime(oxt);
      curItem.GetXTime(cxt);
      if (oxt != cxt) {
        diff_flags |= MRG_XTIME;
        LoadAString(str_temp, IDSC_FLDNMXTIME);
        str_diffs += str_temp + _T(", ");
      }

      otherItem.GetXTimeInt(oxtint);
      curItem.GetXTimeInt(cxtint);
      if (oxtint != cxtint) {
        diff_flags |= MRG_XTIME_INT;
        LoadAString(str_temp, IDSC_FLDNMXTIMEINT);
        str_diffs += str_temp + _T(", ");
      }

      if (otherItem.GetRunCommand() != curItem.GetRunCommand()) {
        diff_flags |= MRG_EXECUTE;
        LoadAString(str_temp, IDSC_FLDNMRUNCOMMAND);
        str_diffs += str_temp + _T(", ");
      }

      // Must use integer values not compare strings
      short other_hDCA, cur_hDCA;
      otherItem.GetDCA(other_hDCA);
      curItem.GetDCA(cur_hDCA);
      if (other_hDCA != cur_hDCA) {
        diff_flags |= MRG_DCA;
        LoadAString(str_temp, IDSC_FLDNMDCA);
        str_diffs += str_temp + _T(", ");
      }

      if (otherItem.GetEmail() != curItem.GetEmail()) {
        diff_flags |= MRG_EMAIL;
        LoadAString(str_temp, IDSC_FLDNMEMAIL);
        str_diffs += str_temp + _T(", ");
      }

      if (otherItem.GetSymbols() != curItem.GetSymbols()) {
        diff_flags |= MRG_SYMBOLS;
        LoadAString(str_temp, IDSC_FLDNMSYMBOLS);
        str_diffs += str_temp + _T(", ");
      }

      otherItem.GetShiftDCA(other_hDCA);
      curItem.GetShiftDCA(cur_hDCA);
      if (other_hDCA != cur_hDCA) {
        diff_flags |= MRG_SHIFTDCA;
        LoadAString(str_temp, IDSC_FLDNMSHIFTDCA);
        str_diffs += str_temp + _T(", ");
      }

      PWPolicy st_to_pp, st_from_pp;
      StringX sxCurrentPolicyName = curItem.GetPolicyName();
      StringX sxOtherPolicyName = otherItem.GetPolicyName();
      bool bCurrent(false), bOther(false);

      if (!sxCurrentPolicyName.empty())
        bCurrent = GetPolicyFromName(sxCurrentPolicyName, st_to_pp);
      if (!sxOtherPolicyName.empty())
        bOther = pothercore->GetPolicyFromName(sxOtherPolicyName, st_from_pp);

      /*
        There will be differences if only one has a named password policy, or
        both have policies but the new entry's one is not in our database, or
        both have the same policy but they are different
      */
      if ((bCurrent && !bOther) || (!bCurrent && bOther) ||
          sxCurrentPolicyName != sxOtherPolicyName ||
          (bCurrent && bOther && st_to_pp != st_from_pp)) {
        diff_flags |= MRG_POLICYNAME;
        LoadAString(str_temp, IDSC_FLDNMPWPOLICYNAME);
        str_diffs += str_temp + _T(", ");
      }

      if (diff_flags != 0) {
        // have a match on group/title/user, but not on other fields
        // add an entry suffixed with -merged-YYYYMMDD-HHMMSS
        StringX sx_newTitle;
        Format(sx_newTitle, L"%ls-%ls-%ls", sx_otherTitle.c_str(), sx_merged.c_str(),
                            str_timestring.c_str());

        // note it as an issue for the user
        stringT strWarnMsg;
        Format(strWarnMsg, IDSC_MERGECONFLICTS,
                       sx_otherGroup.c_str(), sx_otherTitle.c_str(), sx_otherUser.c_str(),
                       sx_otherGroup.c_str(), sx_newTitle.c_str(), sx_otherUser.c_str(),
                       str_diffs.c_str());

        // log it
        if (pRpt != NULL)
          pRpt->WriteLine(strWarnMsg.c_str());

        // Check no conflict of unique uuid
        if (Find(base_uuid) != GetEntryEndIter()) {
          otherItem.CreateUUID();
          otherItem.GetUUID(new_base_uuid);
        }

        // Special processing for password policies (default & named)
        bool bUpdated; // not needed for Merge

        Command *pPolicyCmd = ProcessPolicyName(pothercore, otherItem,
                             mapRenamedPolicies, vs_PoliciesAdded,
                             sxOtherPolicyName, bUpdated,
                             sxMerge_DateTime, IDSC_MERGEPOLICY);

        if (pPolicyCmd != NULL)
          pmulticmds->Add(pPolicyCmd);

        // About to add entry - check keyboard shortcut
        if (bKBShortcutInUse) {
          // Remove it
          otherItem.SetKBShortcut(0);
          //  Tell user via the report
          ItemListIter iter = Find(kbshortcut_uuid);
          if (iter != m_pwlist.end()) {
            StringX sxTemp, sxExistingEntry;
            Format(sxExistingEntry, GROUPTITLEUSERINCHEVRONS,
                iter->second.GetGroup().c_str(), iter->second.GetTitle().c_str(),
                iter->second.GetUser().c_str());
            Format(sxTemp, IDSC_KBSHORTCUT_REMOVED, sx_merged.c_str(), sxMergedEntry.c_str(),
                          sxExistingEntry.c_str(), sx_merged.c_str());
            pRpt->WriteLine(sxTemp.c_str());
          }
        }
        
        otherItem.SetTitle(sx_newTitle);
        otherItem.SetStatus(CItemData::ES_ADDED);
        Command *pcmd = AddEntryCommand::Create(this, otherItem);
        pcmd->SetNoGUINotify();
        pmulticmds->Add(pcmd);

        // Update the Wizard page
        UpdateWizard(sxMergedEntry.c_str());

        numConflicts++;
      }
    } else {
      // Didn't find any match...add it directly
      // Check no conflict of unique uuid
      if (Find(base_uuid) != GetEntryEndIter()) {
        otherItem.CreateUUID();
        otherItem.GetUUID(new_base_uuid);
      }

      // Special processing for password policies (default & named)
      bool bUpdated;  // Not needed for Merge
      StringX sxOtherPolicyName = otherItem.GetPolicyName();

      Command *pPolicyCmd = ProcessPolicyName(pothercore, otherItem,
                           mapRenamedPolicies, vs_PoliciesAdded,
                           sxOtherPolicyName, bUpdated,
                           sxMerge_DateTime, IDSC_MERGEPOLICY);

      if (pPolicyCmd != NULL)
        pmulticmds->Add(pPolicyCmd);

      // About to add entry - check keyboard shortcut
      if (bKBShortcutInUse) {
        // Remove it
        otherItem.SetKBShortcut(0);
        //  Tell user via the report
        ItemListIter iter = Find(kbshortcut_uuid);
        if (iter != m_pwlist.end()) {
          StringX sxTemp, sxExistingEntry;
          Format(sxExistingEntry, GROUPTITLEUSERINCHEVRONS,
                iter->second.GetGroup().c_str(), iter->second.GetTitle().c_str(),
                iter->second.GetUser().c_str());
          Format(sxTemp, IDSC_KBSHORTCUT_REMOVED, sx_merged.c_str(), sxMergedEntry.c_str(),
                        sxExistingEntry.c_str(), sx_merged.c_str());
          pRpt->WriteLine(sxTemp.c_str());
        }
      }
      
      otherItem.SetStatus(CItemData::ES_ADDED);
      Command *pcmd = AddEntryCommand::Create(this, otherItem);
      pcmd->SetNoGUINotify();
      pmulticmds->Add(pcmd);

      StringX sx_added;
      Format(sx_added, GROUPTITLEUSERINCHEVRONS,
                sx_otherGroup.c_str(), sx_otherTitle.c_str(), sx_otherUser.c_str());
      vs_added.push_back(sx_added);

      // Update the Wizard page
      UpdateWizard(sx_added.c_str());
      numAdded++;
    }

    if (et == CItemData::ET_ALIASBASE)
      numAliasesAdded += MergeDependents(pothercore, pmulticmds,
                      base_uuid, new_base_uuid,
                      bTitleRenamed, str_timestring, CItemData::ET_ALIAS,
                      vs_AliasesAdded);

    if (et == CItemData::ET_SHORTCUTBASE)
      numShortcutsAdded += MergeDependents(pothercore, pmulticmds,
                      base_uuid, new_base_uuid,
                      bTitleRenamed, str_timestring, CItemData::ET_SHORTCUT,
                      vs_ShortcutsAdded);
  } // iteration over other core's entries

  stringT str_results;
  if (numAdded > 0 && pRpt != NULL) {
    std::sort(vs_added.begin(), vs_added.end(), MergeSyncGTUCompare);
    stringT str_singular_plural_type, str_singular_plural_verb;
    LoadAString(str_singular_plural_type, numAdded == 1 ? IDSC_ENTRY : IDSC_ENTRIES);
    LoadAString(str_singular_plural_verb, numAdded == 1 ? IDSC_WAS : IDSC_WERE);
    Format(str_results, IDSC_MERGEADDED, str_singular_plural_type.c_str(),
                    str_singular_plural_verb.c_str());
    pRpt->WriteLine(str_results.c_str());
    for (size_t i = 0; i < vs_added.size(); i++) {
      Format(str_results, L"\t%ls", vs_added[i].c_str());
      pRpt->WriteLine(str_results.c_str());
    }
  }

  if (numAliasesAdded > 0 && pRpt != NULL) {
    std::sort(vs_AliasesAdded.begin(), vs_AliasesAdded.end(), MergeSyncGTUCompare);
    stringT str_singular_plural_type, str_singular_plural_verb;
    LoadAString(str_singular_plural_type, numAliasesAdded == 1 ? IDSC_ENTRY : IDSC_ENTRIES);
    LoadAString(str_singular_plural_verb, numAliasesAdded == 1 ? IDSC_WAS : IDSC_WERE);
    Format(str_results, IDSC_MERGEADDED, str_singular_plural_type.c_str(),
                    str_singular_plural_verb.c_str());
    pRpt->WriteLine(str_results.c_str());
    for (size_t i = 0; i < vs_AliasesAdded.size(); i++) {
      Format(str_results, _T("\t%ls"), vs_AliasesAdded[i].c_str());
      pRpt->WriteLine(str_results.c_str());
    }
  }

  if (numShortcutsAdded > 0 && pRpt != NULL) {
    std::sort(vs_ShortcutsAdded.begin(), vs_ShortcutsAdded.end(), MergeSyncGTUCompare);
    stringT str_singular_plural_type, str_singular_plural_verb;
    LoadAString(str_singular_plural_type, numShortcutsAdded == 1 ? IDSC_ENTRY : IDSC_ENTRIES);
    LoadAString(str_singular_plural_verb, numShortcutsAdded == 1 ? IDSC_WAS : IDSC_WERE);
    Format(str_results, IDSC_MERGEADDED, str_singular_plural_type.c_str(),
                    str_singular_plural_verb.c_str());
    pRpt->WriteLine(str_results.c_str());
    for (size_t i = 0; i < vs_ShortcutsAdded.size(); i++) {
      Format(str_results, L"\t%ls", vs_ShortcutsAdded[i].c_str());
      pRpt->WriteLine(str_results.c_str());
    }
  }

  // See if user has cancelled
  if (pbCancel != NULL && *pbCancel) {
    delete pmulticmds;
    return _T("");
  }

  Command *pcmd2 = UpdateGUICommand::Create(this, UpdateGUICommand::WN_REDO,
                                            UpdateGUICommand::GUI_REDO_MERGESYNC);
  pmulticmds->Add(pcmd2);
  Execute(pmulticmds);

  // See if user has cancelled too late - reset flag so incorrect information not given to user
  if (pbCancel != NULL && *pbCancel) {
    *pbCancel = false;
  }

  // Tell the user we're done & provide short merge report
  stringT str_entries, str_conflicts, str_aliases, str_shortcuts;
  int totalAdded = numAdded + numConflicts + numAliasesAdded + numShortcutsAdded;
  LoadAString(str_entries, totalAdded == 1 ? IDSC_ENTRY : IDSC_ENTRIES);
  LoadAString(str_conflicts, numConflicts == 1 ? IDSC_CONFLICT : IDSC_CONFLICTS);
  LoadAString(str_aliases, numAliasesAdded == 1 ? IDSC_ALIAS : IDSC_ALIASES);
  LoadAString(str_shortcuts, numShortcutsAdded == 1 ? IDSC_SHORTCUT : IDSC_SHORTCUTS);

  Format(str_results, IDSC_MERGECOMPLETED,
                   totalAdded, str_entries.c_str(),
                   numConflicts, str_conflicts.c_str(),
                   numAliasesAdded, str_aliases.c_str(),
                   numShortcutsAdded, str_shortcuts.c_str());
  pRpt->WriteLine(str_results.c_str());

  return str_results;
}