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"