File indexing completed on 2024-04-21 03:53:29

0001 /*
0002     This file is part of the KContacts framework.
0003     SPDX-FileCopyrightText: 2003 Helge Deller <deller@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 /*
0009     Useful links:
0010         - http://tldp.org/HOWTO/LDAP-Implementation-HOWTO/schemas.html
0011         - http://www.faqs.org/rfcs/rfc2849.html
0012 
0013     Not yet handled items:
0014         - objectclass microsoftaddressbook
0015                 - info,
0016                 - initials,
0017                 - otherfacsimiletelephonenumber,
0018                 - otherpager,
0019                 - physicaldeliveryofficename,
0020 */
0021 
0022 #include "ldifconverter.h"
0023 #include "address.h"
0024 #include "kcontacts_debug.h"
0025 #include "vcardconverter.h"
0026 
0027 #include "ldif_p.h"
0028 
0029 #include <KCountry>
0030 #include <KLocalizedString>
0031 
0032 #include <QIODevice>
0033 #include <QStringList>
0034 #include <QTextStream>
0035 
0036 using namespace KContacts;
0037 
0038 /* internal functions - do not use !! */
0039 
0040 namespace KContacts
0041 {
0042 /**
0043   @internal
0044 
0045   Evaluates @p fieldname and sets the @p value at the addressee or the address
0046   objects when appropriate.
0047 
0048   @param a The addressee to store information into
0049   @param homeAddr The home address to store respective information into
0050   @param workAddr The work address to store respective information into
0051   @param fieldname LDIF field name to evaluate
0052   @param value The value of the field addressed by @p fieldname
0053 */
0054 void evaluatePair(Addressee &a,
0055                   Address &homeAddr,
0056                   Address &workAddr,
0057                   QString &fieldname,
0058                   QString &value,
0059                   int &birthday,
0060                   int &birthmonth,
0061                   int &birthyear,
0062                   ContactGroup &contactGroup);
0063 }
0064 
0065 /* generate LDIF stream */
0066 
0067 static void ldif_out(QTextStream &t, const QString &formatStr, const QString &value)
0068 {
0069     if (value.isEmpty()) {
0070         return;
0071     }
0072 
0073     const QByteArray txt = Ldif::assembleLine(formatStr, value, 72);
0074 
0075     // write the string
0076     t << QString::fromUtf8(txt) << "\n";
0077 }
0078 
0079 bool LDIFConverter::addresseeAndContactGroupToLDIF(const AddresseeList &addrList, const ContactGroup::List &contactGroupList, QString &str)
0080 {
0081     bool result = addresseeToLDIF(addrList, str);
0082     if (!contactGroupList.isEmpty()) {
0083         result = (contactGroupToLDIF(contactGroupList, str) || result); // order matters
0084     }
0085     return result;
0086 }
0087 
0088 bool LDIFConverter::contactGroupToLDIF(const ContactGroup &contactGroup, QString &str)
0089 {
0090     if (contactGroup.dataCount() <= 0) {
0091         return false;
0092     }
0093     QTextStream t(&str, QIODevice::WriteOnly | QIODevice::Append);
0094 
0095     t << "objectclass: top\n";
0096     t << "objectclass: groupOfNames\n";
0097 
0098     for (int i = 0; i < contactGroup.dataCount(); ++i) {
0099         const ContactGroup::Data &data = contactGroup.data(i);
0100         const QString value = QStringLiteral("cn=%1,mail=%2").arg(data.name(), data.email());
0101         ldif_out(t, QStringLiteral("member"), value);
0102     }
0103 
0104     t << "\n";
0105     return true;
0106 }
0107 
0108 bool LDIFConverter::contactGroupToLDIF(const ContactGroup::List &contactGroupList, QString &str)
0109 {
0110     if (contactGroupList.isEmpty()) {
0111         return false;
0112     }
0113 
0114     bool result = true;
0115     for (const ContactGroup &group : contactGroupList) {
0116         result = (contactGroupToLDIF(group, str) || result); // order matters
0117     }
0118     return result;
0119 }
0120 
0121 bool LDIFConverter::addresseeToLDIF(const AddresseeList &addrList, QString &str)
0122 {
0123     if (addrList.isEmpty()) {
0124         return false;
0125     }
0126 
0127     bool result = true;
0128     for (const Addressee &addr : addrList) {
0129         result = (addresseeToLDIF(addr, str) || result); // order matters
0130     }
0131     return result;
0132 }
0133 
0134 static QString countryName(const QString &isoCodeOrName)
0135 {
0136     const auto c = KCountry::fromAlpha2(isoCodeOrName);
0137     return c.isValid() ? c.name() : isoCodeOrName;
0138 }
0139 
0140 bool LDIFConverter::addresseeToLDIF(const Addressee &addr, QString &str)
0141 {
0142     if (addr.isEmpty()) {
0143         return false;
0144     }
0145 
0146     QTextStream t(&str, QIODevice::WriteOnly | QIODevice::Append);
0147 
0148     const Address homeAddr = addr.address(Address::Home);
0149     const Address workAddr = addr.address(Address::Work);
0150 
0151     ldif_out(t, QStringLiteral("dn"), QStringLiteral("cn=%1,mail=%2").arg(addr.formattedName().simplified(), addr.preferredEmail()));
0152     t << "objectclass: top\n";
0153     t << "objectclass: person\n";
0154     t << "objectclass: organizationalPerson\n";
0155 
0156     ldif_out(t, QStringLiteral("givenname"), addr.givenName());
0157     ldif_out(t, QStringLiteral("sn"), addr.familyName());
0158     ldif_out(t, QStringLiteral("cn"), addr.formattedName().simplified());
0159     ldif_out(t, QStringLiteral("uid"), addr.uid());
0160     ldif_out(t, QStringLiteral("nickname"), addr.nickName());
0161     ldif_out(t, QStringLiteral("xmozillanickname"), addr.nickName());
0162     ldif_out(t, QStringLiteral("mozillanickname"), addr.nickName());
0163 
0164     ldif_out(t, QStringLiteral("mail"), addr.preferredEmail());
0165     const QStringList emails = addr.emails();
0166     const int numEmails = emails.count();
0167     for (int i = 1; i < numEmails; ++i) {
0168         if (i == 0) {
0169             // nothing
0170         } else if (i == 1) {
0171             ldif_out(t, QStringLiteral("mozillasecondemail"), emails[1]);
0172         } else {
0173             ldif_out(t, QStringLiteral("othermailbox"), emails[i]);
0174         }
0175     }
0176     // ldif_out( t, "mozilla_AIMScreenName: %1\n", "screen_name" );
0177 
0178     ldif_out(t, QStringLiteral("telephonenumber"), addr.phoneNumber(PhoneNumber::Work).number());
0179     ldif_out(t, QStringLiteral("facsimiletelephonenumber"), addr.phoneNumber(PhoneNumber::Fax).number());
0180     ldif_out(t, QStringLiteral("homephone"), addr.phoneNumber(PhoneNumber::Home).number());
0181     ldif_out(t, QStringLiteral("mobile"),
0182              addr.phoneNumber(PhoneNumber::Cell).number()); // Netscape 7
0183     ldif_out(t, QStringLiteral("cellphone"),
0184              addr.phoneNumber(PhoneNumber::Cell).number()); // Netscape 4.x
0185     ldif_out(t, QStringLiteral("pager"), addr.phoneNumber(PhoneNumber::Pager).number());
0186     ldif_out(t, QStringLiteral("pagerphone"), addr.phoneNumber(PhoneNumber::Pager).number());
0187 
0188     ldif_out(t, QStringLiteral("streethomeaddress"), homeAddr.street());
0189     ldif_out(t, QStringLiteral("postalcode"), workAddr.postalCode());
0190     ldif_out(t, QStringLiteral("postofficebox"), workAddr.postOfficeBox());
0191 
0192     QStringList streets = homeAddr.street().split(QLatin1Char('\n'));
0193     const int numberOfStreets(streets.count());
0194     if (numberOfStreets > 0) {
0195         ldif_out(t, QStringLiteral("homepostaladdress"), streets.at(0)); // Netscape 7
0196     }
0197     if (numberOfStreets > 1) {
0198         ldif_out(t, QStringLiteral("mozillahomepostaladdress2"), streets.at(1)); // Netscape 7
0199     }
0200     ldif_out(t, QStringLiteral("mozillahomelocalityname"), homeAddr.locality()); // Netscape 7
0201     ldif_out(t, QStringLiteral("mozillahomestate"), homeAddr.region());
0202     ldif_out(t, QStringLiteral("mozillahomepostalcode"), homeAddr.postalCode());
0203     ldif_out(t, QStringLiteral("mozillahomecountryname"), countryName(homeAddr.country()));
0204     ldif_out(t, QStringLiteral("locality"), workAddr.locality());
0205     ldif_out(t, QStringLiteral("streetaddress"), workAddr.street()); // Netscape 4.x
0206 
0207     streets = workAddr.street().split(QLatin1Char('\n'));
0208     const int streetsCount = streets.count();
0209     if (streetsCount > 0) {
0210         ldif_out(t, QStringLiteral("street"), streets.at(0));
0211     }
0212     if (streetsCount > 1) {
0213         ldif_out(t, QStringLiteral("mozillaworkstreet2"), streets.at(1));
0214     }
0215     ldif_out(t, QStringLiteral("countryname"), countryName(workAddr.country()));
0216     ldif_out(t, QStringLiteral("l"), workAddr.locality());
0217     ldif_out(t, QStringLiteral("c"), countryName(workAddr.country()));
0218     ldif_out(t, QStringLiteral("st"), workAddr.region());
0219 
0220     ldif_out(t, QStringLiteral("title"), addr.title());
0221     ldif_out(t, QStringLiteral("vocation"), addr.prefix());
0222     ldif_out(t, QStringLiteral("ou"), addr.role());
0223     ldif_out(t, QStringLiteral("o"), addr.organization());
0224     ldif_out(t, QStringLiteral("organization"), addr.organization());
0225     ldif_out(t, QStringLiteral("organizationname"), addr.organization());
0226 
0227     // Compatibility with older kabc versions.
0228     if (!addr.department().isEmpty()) {
0229         ldif_out(t, QStringLiteral("department"), addr.department());
0230     } else {
0231         ldif_out(t, QStringLiteral("department"), addr.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Department")));
0232     }
0233 
0234     ldif_out(t, QStringLiteral("workurl"), addr.url().url().toDisplayString());
0235     ldif_out(t, QStringLiteral("homeurl"), addr.url().url().toDisplayString());
0236     ldif_out(t, QStringLiteral("mozillahomeurl"), addr.url().url().toDisplayString());
0237 
0238     ldif_out(t, QStringLiteral("description"), addr.note());
0239     if (addr.revision().isValid()) {
0240         ldif_out(t, QStringLiteral("modifytimestamp"), dateToVCardString(addr.revision()));
0241     }
0242 
0243     const QDate birthday = addr.birthday().date();
0244     if (birthday.isValid()) {
0245         const int year = birthday.year();
0246         if (year > 0) {
0247             ldif_out(t, QStringLiteral("birthyear"), QString::number(year));
0248         }
0249         ldif_out(t, QStringLiteral("birthmonth"), QString::number(birthday.month()));
0250         ldif_out(t, QStringLiteral("birthday"), QString::number(birthday.day()));
0251     }
0252 
0253     t << "\n";
0254 
0255     return true;
0256 }
0257 
0258 /* convert from LDIF stream */
0259 bool LDIFConverter::LDIFToAddressee(const QString &str, AddresseeList &addrList, ContactGroup::List &contactGroupList, const QDateTime &dt)
0260 {
0261     if (str.isEmpty()) {
0262         return true;
0263     }
0264 
0265     bool endldif = false;
0266     bool end = false;
0267     Ldif ldif;
0268     Ldif::ParseValue ret;
0269     Addressee a;
0270     Address homeAddr;
0271     Address workAddr;
0272     int birthday = -1;
0273     int birthmonth = -1;
0274     int birthyear = -1;
0275     ContactGroup contactGroup;
0276     ldif.setLdif(str.toLatin1());
0277     QDateTime qdt = dt;
0278     if (!qdt.isValid()) {
0279         qdt = QDateTime::currentDateTime();
0280     }
0281     a.setRevision(qdt);
0282     homeAddr = Address(Address::Home);
0283     workAddr = Address(Address::Work);
0284 
0285     do {
0286         ret = ldif.nextItem();
0287         switch (ret) {
0288         case Ldif::Item: {
0289             QString fieldname = ldif.attr().toLower();
0290             QString value = QString::fromUtf8(ldif.value());
0291             evaluatePair(a, homeAddr, workAddr, fieldname, value, birthday, birthmonth, birthyear, contactGroup);
0292             break;
0293         }
0294         case Ldif::EndEntry:
0295             if (contactGroup.count() == 0) {
0296                 // if the new address is not empty, append it
0297                 QDate birthDate(birthyear, birthmonth, birthday);
0298                 if (birthDate.isValid()) {
0299                     a.setBirthday(birthDate);
0300                 }
0301 
0302                 if (!a.formattedName().isEmpty() || !a.name().isEmpty() || !a.familyName().isEmpty()) {
0303                     if (!homeAddr.isEmpty()) {
0304                         a.insertAddress(homeAddr);
0305                     }
0306                     if (!workAddr.isEmpty()) {
0307                         a.insertAddress(workAddr);
0308                     }
0309                     addrList.append(a);
0310                 }
0311             } else {
0312                 contactGroupList.append(contactGroup);
0313             }
0314             a = Addressee();
0315             contactGroup = ContactGroup();
0316             a.setRevision(qdt);
0317             homeAddr = Address(Address::Home);
0318             workAddr = Address(Address::Work);
0319             break;
0320         case Ldif::MoreData:
0321             if (endldif) {
0322                 end = true;
0323             } else {
0324                 ldif.endLdif();
0325                 endldif = true;
0326                 break;
0327             }
0328         default:
0329             break;
0330         }
0331     } while (!end);
0332 
0333     return true;
0334 }
0335 
0336 void KContacts::evaluatePair(Addressee &a,
0337                              Address &homeAddr,
0338                              Address &workAddr,
0339                              QString &fieldname,
0340                              QString &value,
0341                              int &birthday,
0342                              int &birthmonth,
0343                              int &birthyear,
0344                              ContactGroup &contactGroup)
0345 {
0346     if (fieldname == QLatin1String("dn")) { // ignore
0347         return;
0348     }
0349 
0350     if (fieldname.startsWith(QLatin1Char('#'))) {
0351         return;
0352     }
0353 
0354     if (fieldname.isEmpty() && !a.note().isEmpty()) {
0355         // some LDIF export filters are broken and add additional
0356         // comments on stand-alone lines. Just add them to the notes for now.
0357         a.setNote(a.note() + QLatin1Char('\n') + value);
0358         return;
0359     }
0360 
0361     if (fieldname == QLatin1String("givenname")) {
0362         a.setGivenName(value);
0363         return;
0364     }
0365 
0366     if (fieldname == QLatin1String("xmozillanickname") //
0367         || fieldname == QLatin1String("nickname") //
0368         || fieldname == QLatin1String("mozillanickname")) {
0369         a.setNickName(value);
0370         return;
0371     }
0372 
0373     if (fieldname == QLatin1String("sn")) {
0374         a.setFamilyName(value);
0375         return;
0376     }
0377 
0378     if (fieldname == QLatin1String("uid")) {
0379         a.setUid(value);
0380         return;
0381     }
0382     if (fieldname == QLatin1String("mail") //
0383         || fieldname == QLatin1String("mozillasecondemail") /* mozilla */
0384         || fieldname == QLatin1String("othermailbox") /*TheBat!*/) {
0385         if (a.emails().indexOf(value) == -1) {
0386             a.addEmail(value);
0387         }
0388         return;
0389     }
0390 
0391     if (fieldname == QLatin1String("title")) {
0392         a.setTitle(value);
0393         return;
0394     }
0395 
0396     if (fieldname == QLatin1String("vocation")) {
0397         a.setPrefix(value);
0398         return;
0399     }
0400 
0401     if (fieldname == QLatin1String("cn")) {
0402         a.setFormattedName(value);
0403         return;
0404     }
0405 
0406     if (fieldname == QLatin1Char('o') || fieldname == QLatin1String("organization") // Exchange
0407         || fieldname == QLatin1String("organizationname")) { // Exchange
0408         a.setOrganization(value);
0409         return;
0410     }
0411 
0412     // clang-format off
0413     if (fieldname == QLatin1String("description")
0414         || fieldname == QLatin1String("mozillacustom1")
0415         || fieldname == QLatin1String("mozillacustom2")
0416         || fieldname == QLatin1String("mozillacustom3")
0417         || fieldname == QLatin1String("mozillacustom4")
0418         || fieldname == QLatin1String("custom1")
0419         || fieldname == QLatin1String("custom2")
0420         || fieldname == QLatin1String("custom3")
0421         || fieldname == QLatin1String("custom4")) {
0422         if (!a.note().isEmpty()) {
0423             a.setNote(a.note() + QLatin1Char('\n'));
0424         }
0425         a.setNote(a.note() + value);
0426         return;
0427     }
0428     // clang-format on
0429 
0430     if (fieldname == QLatin1String("homeurl") //
0431         || fieldname == QLatin1String("workurl") //
0432         || fieldname == QLatin1String("mozillahomeurl")) {
0433         if (a.url().url().isEmpty()) {
0434             ResourceLocatorUrl url;
0435             url.setUrl(QUrl(value));
0436             a.setUrl(url);
0437             return;
0438         }
0439         if (a.url().url().toDisplayString() == QUrl(value).toDisplayString()) {
0440             return;
0441         }
0442         // TODO: current version of kabc only supports one URL.
0443         // TODO: change this with KDE 4
0444     }
0445 
0446     if (fieldname == QLatin1String("homephone")) {
0447         a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Home));
0448         return;
0449     }
0450 
0451     if (fieldname == QLatin1String("telephonenumber")) {
0452         a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Work));
0453         return;
0454     }
0455     if (fieldname == QLatin1String("mobile") /* mozilla/Netscape 7 */
0456         || fieldname == QLatin1String("cellphone")) {
0457         a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Cell));
0458         return;
0459     }
0460 
0461     if (fieldname == QLatin1String("pager") // mozilla
0462         || fieldname == QLatin1String("pagerphone")) { // mozilla
0463         a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Pager));
0464         return;
0465     }
0466 
0467     if (fieldname == QLatin1String("facsimiletelephonenumber")) {
0468         a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Fax));
0469         return;
0470     }
0471 
0472     if (fieldname == QLatin1String("xmozillaanyphone")) { // mozilla
0473         a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Work));
0474         return;
0475     }
0476 
0477     if (fieldname == QLatin1String("streethomeaddress") //
0478         || fieldname == QLatin1String("mozillahomestreet")) { // thunderbird
0479         homeAddr.setStreet(value);
0480         return;
0481     }
0482 
0483     if (fieldname == QLatin1String("street") //
0484         || fieldname == QLatin1String("postaladdress")) { // mozilla
0485         workAddr.setStreet(value);
0486         return;
0487     }
0488     if (fieldname == QLatin1String("mozillapostaladdress2") //
0489         || fieldname == QLatin1String("mozillaworkstreet2")) { // mozilla
0490         workAddr.setStreet(workAddr.street() + QLatin1Char('\n') + value);
0491         return;
0492     }
0493 
0494     if (fieldname == QLatin1String("postalcode")) {
0495         workAddr.setPostalCode(value);
0496         return;
0497     }
0498 
0499     if (fieldname == QLatin1String("postofficebox")) {
0500         workAddr.setPostOfficeBox(value);
0501         return;
0502     }
0503 
0504     if (fieldname == QLatin1String("homepostaladdress")) { // Netscape 7
0505         homeAddr.setStreet(value);
0506         return;
0507     }
0508 
0509     if (fieldname == QLatin1String("mozillahomepostaladdress2")) { // mozilla
0510         homeAddr.setStreet(homeAddr.street() + QLatin1Char('\n') + value);
0511         return;
0512     }
0513 
0514     if (fieldname == QLatin1String("mozillahomelocalityname")) { // mozilla
0515         homeAddr.setLocality(value);
0516         return;
0517     }
0518 
0519     if (fieldname == QLatin1String("mozillahomestate")) { // mozilla
0520         homeAddr.setRegion(value);
0521         return;
0522     }
0523 
0524     if (fieldname == QLatin1String("mozillahomepostalcode")) { // mozilla
0525         homeAddr.setPostalCode(value);
0526         return;
0527     }
0528 
0529     if (fieldname == QLatin1String("mozillahomecountryname")) { // mozilla
0530         if (value.length() <= 2) {
0531             value = countryName(value);
0532         }
0533         homeAddr.setCountry(value);
0534         return;
0535     }
0536 
0537     if (fieldname == QLatin1String("locality")) {
0538         workAddr.setLocality(value);
0539         return;
0540     }
0541 
0542     if (fieldname == QLatin1String("streetaddress")) { // Netscape 4.x
0543         workAddr.setStreet(value);
0544         return;
0545     }
0546 
0547     if (fieldname == QLatin1String("countryname") //
0548         || fieldname == QLatin1Char('c')) { // mozilla
0549         if (value.length() <= 2) {
0550             value = countryName(value);
0551         }
0552         workAddr.setCountry(value);
0553         return;
0554     }
0555 
0556     if (fieldname == QLatin1Char('l')) { // mozilla
0557         workAddr.setLocality(value);
0558         return;
0559     }
0560 
0561     if (fieldname == QLatin1String("st")) {
0562         workAddr.setRegion(value);
0563         return;
0564     }
0565 
0566     if (fieldname == QLatin1String("ou")) {
0567         a.setRole(value);
0568         return;
0569     }
0570 
0571     if (fieldname == QLatin1String("department")) {
0572         a.setDepartment(value);
0573         return;
0574     }
0575 
0576     if (fieldname == QLatin1String("member")) {
0577         // this is a mozilla list member (cn=xxx, mail=yyy)
0578         const QStringList list = value.split(QLatin1Char(','));
0579         QString name;
0580         QString email;
0581 
0582         const QLatin1String cnTag("cn=");
0583         const QLatin1String mailTag("mail=");
0584         for (const auto &str : list) {
0585             if (str.startsWith(cnTag)) {
0586                 name = QStringView(str).mid(cnTag.size()).trimmed().toString();
0587             } else if (str.startsWith(mailTag)) {
0588                 email = QStringView(str).mid(mailTag.size()).trimmed().toString();
0589             }
0590         }
0591 
0592         if (!name.isEmpty() && !email.isEmpty()) {
0593             email = QLatin1String(" <") + email + QLatin1Char('>');
0594         }
0595         ContactGroup::Data data;
0596         data.setEmail(email);
0597         data.setName(name);
0598         contactGroup.append(data);
0599         return;
0600     }
0601 
0602     if (fieldname == QLatin1String("modifytimestamp")) {
0603         if (value == QLatin1String("0Z")) { // ignore
0604             return;
0605         }
0606         QDateTime dt = VCardStringToDate(value);
0607         if (dt.isValid()) {
0608             a.setRevision(dt);
0609             return;
0610         }
0611     }
0612 
0613     if (fieldname == QLatin1String("display-name")) {
0614         contactGroup.setName(value);
0615         return;
0616     }
0617 
0618     if (fieldname == QLatin1String("objectclass")) { // ignore
0619         return;
0620     }
0621 
0622     if (fieldname == QLatin1String("birthyear")) {
0623         bool ok;
0624         birthyear = value.toInt(&ok);
0625         if (!ok) {
0626             birthyear = -1;
0627         }
0628         return;
0629     }
0630     if (fieldname == QLatin1String("birthmonth")) {
0631         birthmonth = value.toInt();
0632         return;
0633     }
0634     if (fieldname == QLatin1String("birthday")) {
0635         birthday = value.toInt();
0636         return;
0637     }
0638     if (fieldname == QLatin1String("xbatbirthday")) {
0639         const QStringView str{value};
0640         QDate dt(str.mid(0, 4).toInt(), str.mid(4, 2).toInt(), str.mid(6, 2).toInt());
0641         if (dt.isValid()) {
0642             a.setBirthday(dt);
0643         }
0644         return;
0645     }
0646     qCWarning(KCONTACTS_LOG) << QStringLiteral("LDIFConverter: Unknown field for '%1': '%2=%3'\n").arg(a.formattedName(), fieldname, value);
0647 }