File indexing completed on 2025-01-05 04:49:19

0001 /*
0002    SPDX-FileCopyrightText: 2016-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "vcardimportexportplugininterface.h"
0008 #include <KActionCollection>
0009 #include <KAddressBookImportExport/ContactSelectionDialog>
0010 #include <KContacts/VCardConverter>
0011 #include <KJobWidgets>
0012 #include <KLocalizedString>
0013 #include <KMessageBox>
0014 #include <PimCommon/RenameFileDialog>
0015 #include <QAction>
0016 #include <QFileDialog>
0017 #include <QPointer>
0018 #include <QUrl>
0019 #include <kio/filecopyjob.h>
0020 #include <kio/storedtransferjob.h>
0021 
0022 #ifdef QGPGME_FOUND
0023 #include <KAddressBookImportExport/ImportExportEngine>
0024 #include <QTemporaryFile>
0025 #include <gpgme++/context.h>
0026 #include <gpgme++/data.h>
0027 #include <gpgme++/key.h>
0028 #include <qgpgme/dataprovider.h>
0029 #endif // QGPGME_FOUND
0030 
0031 using namespace KAddressBookImportExport;
0032 
0033 VCardImportExportPluginInterface::VCardImportExportPluginInterface(QObject *parent)
0034     : PluginInterface(parent)
0035     , mExportVCardType(VCard3)
0036 {
0037 }
0038 
0039 VCardImportExportPluginInterface::~VCardImportExportPluginInterface() = default;
0040 
0041 void VCardImportExportPluginInterface::createAction(KActionCollection *ac)
0042 {
0043     QAction *action = ac->addAction(QStringLiteral("file_import_vcard"));
0044     action->setText(i18n("Import vCard..."));
0045     action->setWhatsThis(i18n("Import contacts from a vCard file."));
0046     connect(action, &QAction::triggered, this, &VCardImportExportPluginInterface::slotImportVCard);
0047     setImportActions(QList<QAction *>() << action);
0048 
0049     QList<QAction *> exportActionList;
0050 
0051     action = ac->addAction(QStringLiteral("file_export_vcard40"));
0052     action->setWhatsThis(i18n("Export contacts to a vCard 4.0 file."));
0053     action->setText(i18n("Export vCard 4.0..."));
0054     connect(action, &QAction::triggered, this, &VCardImportExportPluginInterface::slotExportVCard4);
0055     exportActionList << action;
0056 
0057     action = ac->addAction(QStringLiteral("file_export_vcard30"));
0058     action->setText(i18n("Export vCard 3.0..."));
0059     action->setWhatsThis(i18n("Export contacts to a vCard 3.0 file."));
0060     connect(action, &QAction::triggered, this, &VCardImportExportPluginInterface::slotExportVCard3);
0061     exportActionList << action;
0062 
0063     action = ac->addAction(QStringLiteral("file_export_vcard21"));
0064     action->setText(i18n("Export vCard 2.1..."));
0065     action->setWhatsThis(i18n("Export contacts to a vCard 2.1 file."));
0066     connect(action, &QAction::triggered, this, &VCardImportExportPluginInterface::slotExportVCard2);
0067     exportActionList << action;
0068     setExportActions(exportActionList);
0069 }
0070 
0071 void VCardImportExportPluginInterface::slotExportVCard4()
0072 {
0073     mImportExportAction = Export;
0074     mExportVCardType = VCard4;
0075     Q_EMIT emitPluginActivated(this);
0076 }
0077 
0078 void VCardImportExportPluginInterface::slotExportVCard3()
0079 {
0080     mImportExportAction = Export;
0081     mExportVCardType = VCard3;
0082     Q_EMIT emitPluginActivated(this);
0083 }
0084 
0085 void VCardImportExportPluginInterface::slotExportVCard2()
0086 {
0087     mImportExportAction = Export;
0088     mExportVCardType = VCard2_1;
0089     Q_EMIT emitPluginActivated(this);
0090 }
0091 
0092 void VCardImportExportPluginInterface::exec()
0093 {
0094     switch (mImportExportAction) {
0095     case Import:
0096         importVCard();
0097         break;
0098     case Export:
0099         exportVCard();
0100         break;
0101     }
0102 }
0103 
0104 void VCardImportExportPluginInterface::slotImportVCard()
0105 {
0106     mImportExportAction = Import;
0107     Q_EMIT emitPluginActivated(this);
0108 }
0109 
0110 void VCardImportExportPluginInterface::importVCard()
0111 {
0112     KContacts::Addressee::List addrList;
0113     const QString filter = i18n("vCard (*.vcf *.vcard *.vct *.gcrd);;All files (*)");
0114     const QList<QUrl> urls = QFileDialog::getOpenFileUrls(parentWidget(), i18nc("@title:window", "Select vCard to Import"), QUrl(), filter);
0115 
0116     if (urls.isEmpty()) {
0117         return;
0118     }
0119 
0120     const QString caption(i18nc("@title:window", "vCard Import Failed"));
0121 
0122     const int numberOfUrl(urls.count());
0123     for (int i = 0; i < numberOfUrl; ++i) {
0124         const QUrl url = urls.at(i);
0125 
0126         auto job = KIO::storedGet(url);
0127         KJobWidgets::setWindow(job, parentWidget());
0128         if (job->exec()) {
0129             const QByteArray data = job->data();
0130             if (!data.isEmpty()) {
0131                 addrList += parseVCard(data);
0132             }
0133         } else {
0134             const QString msg = xi18nc("@info", "<para>Unable to access vCard:</para><para>%1</para>", job->errorString());
0135             KMessageBox::error(parentWidget(), msg, caption);
0136         }
0137     }
0138     ContactList contactList;
0139     contactList.setAddressList(addrList);
0140     auto engine = new ImportExportEngine(this);
0141     engine->setContactList(contactList);
0142     engine->setDefaultAddressBook(defaultCollection());
0143     engine->importContacts();
0144 }
0145 
0146 KContacts::Addressee::List VCardImportExportPluginInterface::parseVCard(const QByteArray &data) const
0147 {
0148     KContacts::VCardConverter converter;
0149     return converter.parseVCards(data);
0150 }
0151 
0152 KContacts::Addressee::List VCardImportExportPluginInterface::filterContacts(const KContacts::Addressee::List &addrList,
0153                                                                             ExportSelectionWidget::ExportFields exportFieldType) const
0154 {
0155     KContacts::Addressee::List list;
0156 
0157     if (addrList.isEmpty()) {
0158         return addrList;
0159     }
0160 
0161     KContacts::Addressee::List::ConstIterator it;
0162     KContacts::Addressee::List::ConstIterator end(addrList.end());
0163     for (it = addrList.begin(); it != end; ++it) {
0164         KContacts::Addressee addr;
0165 
0166         addr.setUid(QString());
0167         addr.setFormattedName((*it).formattedName());
0168 
0169         bool addrDone = false;
0170         if (exportFieldType & ExportSelectionWidget::DiplayName) { // output display name as N field
0171             QString fmtName = (*it).formattedName();
0172             QStringList splitNames = fmtName.split(QLatin1Char(' '), Qt::SkipEmptyParts);
0173             if (splitNames.count() >= 2) {
0174                 addr.setPrefix(QString());
0175                 addr.setGivenName(splitNames.takeFirst());
0176                 addr.setFamilyName(splitNames.takeLast());
0177                 addr.setAdditionalName(splitNames.join(QLatin1Char(' ')));
0178                 addr.setSuffix(QString());
0179                 addrDone = true;
0180             }
0181         }
0182 
0183         if (!addrDone) { // not wanted, or could not be split
0184             addr.setPrefix((*it).prefix());
0185             addr.setGivenName((*it).givenName());
0186             addr.setAdditionalName((*it).additionalName());
0187             addr.setFamilyName((*it).familyName());
0188             addr.setSuffix((*it).suffix());
0189         }
0190 
0191         addr.setExtraNickNameList((*it).extraNickNameList());
0192         addr.setMailer((*it).mailer());
0193         addr.setTimeZone((*it).timeZone());
0194         addr.setGeo((*it).geo());
0195         addr.setProductId((*it).productId());
0196         addr.setSortString((*it).sortString());
0197         addr.setUrl((*it).url());
0198         addr.setExtraUrlList((*it).extraUrlList());
0199         addr.setSecrecy((*it).secrecy());
0200         addr.setSound((*it).sound());
0201         addr.setEmailList((*it).emailList());
0202         addr.setCategories((*it).categories());
0203         addr.setExtraSoundList((*it).extraSoundList());
0204         addr.setGender((*it).gender());
0205         addr.setLangs((*it).langs());
0206         addr.setKind((*it).kind());
0207         addr.setMembers((*it).members());
0208         addr.setRelationships((*it).relationships());
0209         addr.setSourcesUrlList((*it).sourcesUrlList());
0210         addr.setImppList((*it).imppList());
0211         addr.setFieldGroupList((*it).fieldGroupList());
0212 
0213         if (exportFieldType & ExportSelectionWidget::Private) {
0214             addr.setBirthday((*it).birthday());
0215             addr.setNote((*it).note());
0216         }
0217 
0218         if (exportFieldType & ExportSelectionWidget::Picture) {
0219             if (exportFieldType & ExportSelectionWidget::Private) {
0220                 addr.setPhoto((*it).photo());
0221                 addr.setExtraPhotoList((*it).extraPhotoList());
0222             }
0223 
0224             if (exportFieldType & ExportSelectionWidget::Business) {
0225                 addr.setLogo((*it).logo());
0226                 addr.setExtraLogoList((*it).extraLogoList());
0227             }
0228         }
0229 
0230         if (exportFieldType & ExportSelectionWidget::Business) {
0231             addr.setExtraTitleList((*it).extraTitleList());
0232             addr.setExtraRoleList((*it).extraRoleList());
0233             addr.setExtraOrganizationList((*it).extraOrganizationList());
0234             addr.setDepartment((*it).department());
0235 
0236             KContacts::PhoneNumber::List phones = (*it).phoneNumbers(KContacts::PhoneNumber::Work);
0237             KContacts::PhoneNumber::List::Iterator phoneIt;
0238             KContacts::PhoneNumber::List::Iterator phoneEnd(phones.end());
0239             for (phoneIt = phones.begin(); phoneIt != phoneEnd; ++phoneIt) {
0240                 addr.insertPhoneNumber(*phoneIt);
0241             }
0242 
0243             KContacts::Address::List addresses = (*it).addresses(KContacts::Address::Work);
0244             KContacts::Address::List::Iterator addrIt;
0245             KContacts::Address::List::Iterator addrEnd(addresses.end());
0246             for (addrIt = addresses.begin(); addrIt != addrEnd; ++addrIt) {
0247                 addr.insertAddress(*addrIt);
0248             }
0249         }
0250 
0251         KContacts::PhoneNumber::List phones = (*it).phoneNumbers();
0252         KContacts::PhoneNumber::List::Iterator phoneIt;
0253         KContacts::PhoneNumber::List::Iterator phoneEnd(phones.end());
0254         for (phoneIt = phones.begin(); phoneIt != phoneEnd; ++phoneIt) {
0255             int phoneType = (*phoneIt).type();
0256 
0257             if ((phoneType & KContacts::PhoneNumber::Home) && (exportFieldType & ExportSelectionWidget::Private)) {
0258                 addr.insertPhoneNumber(*phoneIt);
0259             } else if ((phoneType & KContacts::PhoneNumber::Work) && (exportFieldType & ExportSelectionWidget::Business)) {
0260                 addr.insertPhoneNumber(*phoneIt);
0261             } else if ((exportFieldType & ExportSelectionWidget::Other)) {
0262                 addr.insertPhoneNumber(*phoneIt);
0263             }
0264         }
0265 
0266         KContacts::Address::List addresses = (*it).addresses();
0267         KContacts::Address::List::Iterator addrIt;
0268         KContacts::Address::List::Iterator addrEnd(addresses.end());
0269         for (addrIt = addresses.begin(); addrIt != addrEnd; ++addrIt) {
0270             int addressType = (*addrIt).type();
0271 
0272             if ((addressType & KContacts::Address::Home) && exportFieldType & ExportSelectionWidget::Private) {
0273                 addr.insertAddress(*addrIt);
0274             } else if ((addressType & KContacts::Address::Work) && (exportFieldType & ExportSelectionWidget::Business)) {
0275                 addr.insertAddress(*addrIt);
0276             } else if (exportFieldType & ExportSelectionWidget::Other) {
0277                 addr.insertAddress(*addrIt);
0278             }
0279         }
0280 
0281         if (exportFieldType & ExportSelectionWidget::Other) {
0282             QStringList exportFields;
0283             const QStringList customs = (*it).customs();
0284             for (const QString &customStr : customs) {
0285                 if (!customStr.startsWith(QLatin1StringView("X-GCALENDAR-groupMembershipInfo"))) {
0286                     exportFields.append(customStr);
0287                 }
0288             }
0289             addr.setCustoms(exportFields);
0290         }
0291 
0292         if (exportFieldType & ExportSelectionWidget::Encryption) {
0293             addKey(addr, KContacts::Key::PGP);
0294             addKey(addr, KContacts::Key::X509);
0295         }
0296 
0297         list.append(addr);
0298     }
0299 
0300     return list;
0301 }
0302 
0303 void VCardImportExportPluginInterface::addKey(KContacts::Addressee &addr, KContacts::Key::Type type) const
0304 {
0305 #ifdef QGPGME_FOUND
0306     const QString fingerprint =
0307         addr.custom(QStringLiteral("KADDRESSBOOK"), (type == KContacts::Key::PGP ? QStringLiteral("OPENPGPFP") : QStringLiteral("SMIMEFP")));
0308     if (fingerprint.isEmpty()) {
0309         return;
0310     }
0311 
0312     GpgME::Context *context = GpgME::Context::createForProtocol(GpgME::OpenPGP);
0313     if (!context) {
0314         qCritical() << "No context available";
0315         return;
0316     }
0317 
0318     context->setArmor(false);
0319     context->setTextMode(false);
0320 
0321     QGpgME::QByteArrayDataProvider dataProvider;
0322     GpgME::Data dataObj(&dataProvider);
0323     GpgME::Error error = context->exportPublicKeys(fingerprint.toLatin1().constData(), dataObj);
0324     delete context;
0325 
0326     if (error) {
0327         qCritical() << error.asString();
0328         return;
0329     }
0330 
0331     KContacts::Key key;
0332     key.setType(type);
0333     key.setBinaryData(dataProvider.data());
0334 
0335     addr.insertKey(key);
0336 #else
0337     Q_UNUSED(addr)
0338     Q_UNUSED(type)
0339 #endif
0340 }
0341 
0342 bool VCardImportExportPluginInterface::doExport(const QUrl &url, const QByteArray &data) const
0343 {
0344     QUrl newUrl(url);
0345     if (newUrl.isLocalFile() && QFileInfo::exists(newUrl.toLocalFile())) {
0346         auto dialog = new PimCommon::RenameFileDialog(newUrl, false, parentWidget());
0347         auto result = static_cast<PimCommon::RenameFileDialog::RenameFileDialogResult>(dialog->exec());
0348         if (result == PimCommon::RenameFileDialog::RENAMEFILE_RENAME) {
0349             newUrl = dialog->newName();
0350         } else if (result == PimCommon::RenameFileDialog::RENAMEFILE_IGNORE) {
0351             delete dialog;
0352             return true;
0353         }
0354         delete dialog;
0355     }
0356 
0357     QTemporaryFile tmpFile;
0358     tmpFile.open();
0359 
0360     tmpFile.write(data);
0361     tmpFile.flush();
0362     auto job = KIO::file_copy(QUrl::fromLocalFile(tmpFile.fileName()), newUrl, -1, KIO::Overwrite);
0363     KJobWidgets::setWindow(job, parentWidget());
0364     return job->exec();
0365 }
0366 
0367 void VCardImportExportPluginInterface::exportVCard()
0368 {
0369     QPointer<ContactSelectionDialog> dlg = new ContactSelectionDialog(itemSelectionModel(), true, parentWidget());
0370     dlg->setMessageText(i18n("Which contact do you want to export?"));
0371     dlg->setDefaultAddressBook(defaultCollection());
0372     if (!dlg->exec() || !dlg) {
0373         delete dlg;
0374         return;
0375     }
0376 
0377     const KContacts::AddresseeList contacts = dlg->selectedContacts().addressList();
0378     const ExportSelectionWidget::ExportFields exportFields = dlg->exportType();
0379     delete dlg;
0380 
0381     if (contacts.isEmpty()) {
0382         KMessageBox::error(nullptr, i18n("You have not selected any contacts to export."));
0383         return;
0384     }
0385 
0386     KContacts::VCardConverter converter;
0387     QUrl url;
0388 
0389     const KContacts::Addressee::List list = filterContacts(contacts, exportFields);
0390     if (list.isEmpty()) { // no contact selected
0391         return;
0392     }
0393 
0394     bool ok = true;
0395     if (list.count() == 1) {
0396         QFileDialog::Options options = QFileDialog::DontConfirmOverwrite;
0397         QString filename;
0398         const KContacts::Addressee addr = list.at(0);
0399         if (!addr.givenName().isEmpty()) {
0400             filename = addr.givenName();
0401         }
0402         if (!addr.familyName().isEmpty()) {
0403             if (filename.isEmpty()) {
0404                 filename = addr.familyName();
0405             } else {
0406                 filename += QLatin1Char('_') + addr.familyName();
0407             }
0408         }
0409         if (filename.isEmpty()) {
0410             if (!addr.emailList().isEmpty()) {
0411                 filename = addr.emailList().at(0).mail();
0412             } else {
0413                 filename = QStringLiteral("contact");
0414             }
0415         }
0416         filename += QStringLiteral(".vcf");
0417         url = QFileDialog::getSaveFileUrl(parentWidget(), QString(), QUrl::fromLocalFile(filename), QString(), nullptr, options);
0418         if (url.isEmpty()) { // user canceled export
0419             return;
0420         }
0421 
0422         switch (mExportVCardType) {
0423         case VCard2_1:
0424             ok = doExport(url, converter.exportVCards(list, KContacts::VCardConverter::v2_1));
0425             break;
0426         case VCard3:
0427             ok = doExport(url, converter.exportVCards(list, KContacts::VCardConverter::v3_0));
0428             break;
0429         case VCard4:
0430             ok = doExport(url, converter.exportVCards(list, KContacts::VCardConverter::v4_0));
0431             break;
0432         }
0433     } else {
0434         const int answer = KMessageBox::questionTwoActionsCancel(parentWidget(),
0435                                                                  i18nc("@info",
0436                                                                        "You have selected a list of contacts, "
0437                                                                        "shall they be exported to several files?"),
0438                                                                  QString(),
0439                                                                  KGuiItem(i18nc("@action:button", "Export to One File")),
0440                                                                  KGuiItem(i18nc("@action:button", "Export to Several Files")));
0441 
0442         switch (answer) {
0443         case KMessageBox::ButtonCode::SecondaryAction: {
0444             const QUrl baseUrl = QFileDialog::getExistingDirectoryUrl();
0445             if (baseUrl.isEmpty()) {
0446                 return; // user canceled export
0447             }
0448 
0449             for (int i = 0; i < list.count(); ++i) {
0450                 const KContacts::Addressee contact = list.at(i);
0451 
0452                 url = QUrl::fromLocalFile(baseUrl.path() + QLatin1Char('/') + contactFileName(contact) + QStringLiteral(".vcf"));
0453 
0454                 bool tmpOk = false;
0455 
0456                 switch (mExportVCardType) {
0457                 case VCard2_1:
0458                     tmpOk = doExport(url, converter.exportVCard(contact, KContacts::VCardConverter::v2_1));
0459                     break;
0460                 case VCard3:
0461                     tmpOk = doExport(url, converter.exportVCard(contact, KContacts::VCardConverter::v3_0));
0462                     break;
0463                 case VCard4:
0464                     tmpOk = doExport(url, converter.exportVCard(contact, KContacts::VCardConverter::v4_0));
0465                     break;
0466                 }
0467                 ok = ok && tmpOk;
0468             }
0469             break;
0470         }
0471         case KMessageBox::ButtonCode::PrimaryAction: {
0472             QFileDialog::Options options = QFileDialog::DontConfirmOverwrite;
0473             url = QFileDialog::getSaveFileUrl(parentWidget(), QString(), QUrl::fromLocalFile(QStringLiteral("addressbook.vcf")), QString(), nullptr, options);
0474             if (url.isEmpty()) {
0475                 return; // user canceled export
0476             }
0477 
0478             switch (mExportVCardType) {
0479             case VCard2_1:
0480                 ok = doExport(url, converter.exportVCards(list, KContacts::VCardConverter::v2_1));
0481                 break;
0482             case VCard3:
0483                 ok = doExport(url, converter.exportVCards(list, KContacts::VCardConverter::v3_0));
0484                 break;
0485             case VCard4:
0486                 ok = doExport(url, converter.exportVCards(list, KContacts::VCardConverter::v4_0));
0487                 break;
0488             }
0489             break;
0490         }
0491         case KMessageBox::Cancel:
0492         default:
0493             return; // user canceled export
0494         }
0495     }
0496 }
0497 
0498 bool VCardImportExportPluginInterface::canImportFileType(const QUrl &url)
0499 {
0500     return url.path().endsWith(QLatin1StringView(".vcf"));
0501 }
0502 
0503 QString VCardImportExportPluginInterface::contactFileName(const KContacts::Addressee &contact) const
0504 {
0505     if (!contact.givenName().isEmpty() && !contact.familyName().isEmpty()) {
0506         return QStringLiteral("%1_%2").arg(contact.givenName(), contact.familyName());
0507     }
0508 
0509     if (!contact.familyName().isEmpty()) {
0510         return contact.familyName();
0511     }
0512 
0513     if (!contact.givenName().isEmpty()) {
0514         return contact.givenName();
0515     }
0516 
0517     if (!contact.organization().isEmpty()) {
0518         return contact.organization();
0519     }
0520 
0521     return contact.uid();
0522 }
0523 
0524 #include "moc_vcardimportexportplugininterface.cpp"