1 /*
2     This file is part of the KContacts framework.
3     SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "address.h"
9 
10 #include "kcontacts_debug.h"
11 #include <KConfig>
12 #include <KCountry>
13 #include <KLocalizedString>
14 #include <krandom.h>
15 
16 #include <KConfigGroup>
17 
18 #include <QDataStream>
19 #include <QLocale>
20 #include <QSharedData>
21 #include <QStandardPaths>
22 
23 using namespace KContacts;
24 
25 // template tags for address formatting localization
26 #define KCONTACTS_FMTTAG_realname QStringLiteral("%n")
27 #define KCONTACTS_FMTTAG_REALNAME QStringLiteral("%N")
28 #define KCONTACTS_FMTTAG_company QStringLiteral("%cm")
29 #define KCONTACTS_FMTTAG_COMPANY QStringLiteral("%CM")
30 #define KCONTACTS_FMTTAG_pobox QStringLiteral("%p")
31 #define KCONTACTS_FMTTAG_street QStringLiteral("%s")
32 #define KCONTACTS_FMTTAG_STREET QStringLiteral("%S")
33 #define KCONTACTS_FMTTAG_zipcode QStringLiteral("%z")
34 #define KCONTACTS_FMTTAG_location QStringLiteral("%l")
35 #define KCONTACTS_FMTTAG_LOCATION QStringLiteral("%L")
36 #define KCONTACTS_FMTTAG_region QStringLiteral("%r")
37 #define KCONTACTS_FMTTAG_REGION QStringLiteral("%R")
38 #define KCONTACTS_FMTTAG_newline QStringLiteral("\\n")
39 #define KCONTACTS_FMTTAG_condcomma QStringLiteral("%,")
40 #define KCONTACTS_FMTTAG_condwhite QStringLiteral("%w")
41 #define KCONTACTS_FMTTAG_purgeempty QStringLiteral("%0")
42 
43 /**
44   Finds the balanced closing bracket starting from the opening bracket at
45   pos in tsection.
46   @return  position of closing bracket, -1 for unbalanced brackets
47 */
findBalancedBracket(const QString & tsection,int pos)48 static int findBalancedBracket(const QString &tsection, int pos)
49 {
50     int balancecounter = 0;
51     for (int i = pos + 1; i < tsection.length(); ++i) {
52         if (QLatin1Char(')') == tsection[i] && 0 == balancecounter) {
53             // found end of brackets
54             return i;
55         } else {
56             if (QLatin1Char('(') == tsection[i]) {
57                 // nested brackets
58                 balancecounter++;
59             }
60         }
61     }
62     return -1;
63 }
64 
65 /**
66   Parses a snippet of an address template
67   @param tsection   the template string to be parsed
68   @param result     QString reference in which the result will be stored
69   @return           true if at least one tag evaluated positively, else false
70 */
71 static bool
parseAddressTemplateSection(const QString & tsection,QString & result,const QString & realName,const QString & orgaName,const KContacts::Address & address)72 parseAddressTemplateSection(const QString &tsection, QString &result, const QString &realName, const QString &orgaName, const KContacts::Address &address)
73 {
74     // This method first parses and substitutes any bracketed sections and
75     // after that replaces any tags with their values. If a bracketed section
76     // or a tag evaluate to zero, they are not just removed but replaced
77     // with a placeholder. This is because in the last step conditionals are
78     // resolved which depend on information about zero-evaluations.
79     result = tsection;
80     int stpos = 0;
81     bool ret = false;
82 
83     // first check for brackets that have to be evaluated first
84     int fpos = result.indexOf(KCONTACTS_FMTTAG_purgeempty, stpos);
85     while (-1 != fpos) {
86         int bpos1 = fpos + KCONTACTS_FMTTAG_purgeempty.length();
87         // expect opening bracket and find next balanced closing bracket. If
88         // next char is no opening bracket, continue parsing (no valid tag)
89         if (QLatin1Char('(') == result[bpos1]) {
90             int bpos2 = findBalancedBracket(result, bpos1);
91             if (-1 != bpos2) {
92                 // we have balanced brackets, recursively parse:
93                 QString rplstr;
94                 bool purge = !parseAddressTemplateSection(result.mid(bpos1 + 1, bpos2 - bpos1 - 1), //
95                                                           rplstr,
96                                                           realName,
97                                                           orgaName,
98                                                           address);
99                 if (purge) {
100                     // purge -> remove all
101                     // replace with !_P_!, so conditional tags work later
102                     result.replace(fpos, bpos2 - fpos + 1, QStringLiteral("!_P_!"));
103                     // leave stpos as it is
104                 } else {
105                     // no purge -> replace with recursively parsed string
106                     result.replace(fpos, bpos2 - fpos + 1, rplstr);
107                     ret = true;
108                     stpos = fpos + rplstr.length();
109                 }
110             } else {
111                 // unbalanced brackets:  keep on parsing (should not happen
112                 // and will result in bad formatting)
113                 stpos = bpos1;
114             }
115         }
116         fpos = result.indexOf(KCONTACTS_FMTTAG_purgeempty, stpos);
117     }
118 
119     // after sorting out all purge tags, we just search'n'replace the rest,
120     // keeping track of whether at least one tag evaluates to something.
121     // The following macro needs QString for R_FIELD
122     // It substitutes !_P_! for empty fields so conditional tags work later
123     // clang-format off
124 #define REPLTAG(R_TAG, R_FIELD) \
125     if (result.contains(R_TAG)) { \
126         QString rpl = R_FIELD.isEmpty() ? QStringLiteral("!_P_!") : R_FIELD; \
127         result.replace(R_TAG, rpl); \
128         if (!R_FIELD.isEmpty()) { \
129             ret = true; \
130         } \
131     }
132     // clang-format on
133     REPLTAG(KCONTACTS_FMTTAG_realname, realName);
134     REPLTAG(KCONTACTS_FMTTAG_REALNAME, realName.toUpper());
135     REPLTAG(KCONTACTS_FMTTAG_company, orgaName);
136     REPLTAG(KCONTACTS_FMTTAG_COMPANY, orgaName.toUpper());
137     REPLTAG(KCONTACTS_FMTTAG_pobox, address.postOfficeBox());
138     REPLTAG(KCONTACTS_FMTTAG_street, address.street());
139     REPLTAG(KCONTACTS_FMTTAG_STREET, address.street().toUpper());
140     REPLTAG(KCONTACTS_FMTTAG_zipcode, address.postalCode());
141     REPLTAG(KCONTACTS_FMTTAG_location, address.locality());
142     REPLTAG(KCONTACTS_FMTTAG_LOCATION, address.locality().toUpper());
143     REPLTAG(KCONTACTS_FMTTAG_region, address.region());
144     REPLTAG(KCONTACTS_FMTTAG_REGION, address.region().toUpper());
145     result.replace(KCONTACTS_FMTTAG_newline, QLatin1String("\n"));
146 #undef REPLTAG
147 
148     // conditional comma
149     fpos = result.indexOf(KCONTACTS_FMTTAG_condcomma, 0);
150     while (-1 != fpos) {
151         const QString str1 = result.mid(fpos - 5, 5);
152         const QString str2 = result.mid(fpos + 2, 5);
153         if (str1 != QLatin1String("!_P_!") && str2 != QLatin1String("!_P_!")) {
154             result.replace(fpos, 2, QStringLiteral(", "));
155         } else {
156             result.remove(fpos, 2);
157         }
158         fpos = result.indexOf(KCONTACTS_FMTTAG_condcomma, fpos);
159     }
160     // conditional whitespace
161     fpos = result.indexOf(KCONTACTS_FMTTAG_condwhite, 0);
162     while (-1 != fpos) {
163         const QString str1 = result.mid(fpos - 5, 5);
164         const QString str2 = result.mid(fpos + 2, 5);
165         if (str1 != QLatin1String("!_P_!") && str2 != QLatin1String("!_P_!")) {
166             result.replace(fpos, 2, QLatin1Char(' '));
167         } else {
168             result.remove(fpos, 2);
169         }
170         fpos = result.indexOf(KCONTACTS_FMTTAG_condwhite, fpos);
171     }
172 
173     // remove purged:
174     result.remove(QStringLiteral("!_P_!"));
175 
176     return ret;
177 }
178 
179 class Q_DECL_HIDDEN Address::Private : public QSharedData
180 {
181 public:
Private()182     Private()
183         : mEmpty(true)
184     {
185         mId = KRandom::randomString(10);
186     }
187 
Private(const Private & other)188     Private(const Private &other)
189         : QSharedData(other)
190     {
191         mEmpty = other.mEmpty;
192         mId = other.mId;
193         mType = other.mType;
194 
195         mPostOfficeBox = other.mPostOfficeBox;
196         mExtended = other.mExtended;
197         mStreet = other.mStreet;
198         mLocality = other.mLocality;
199         mRegion = other.mRegion;
200         mPostalCode = other.mPostalCode;
201         mCountry = other.mCountry;
202         mLabel = other.mLabel;
203     }
204 
205     bool mEmpty;
206     QString mId;
207     Type mType;
208     Geo mGeo;
209 
210     QString mPostOfficeBox;
211     QString mExtended;
212     QString mStreet;
213     QString mLocality;
214     QString mRegion;
215     QString mPostalCode;
216     QString mCountry;
217     QString mLabel;
218 };
219 
Address()220 Address::Address()
221     : d(new Private)
222 {
223 }
224 
Address(Type type)225 Address::Address(Type type)
226     : d(new Private)
227 {
228     d->mType = type;
229 }
230 
Address(const Address & other)231 Address::Address(const Address &other)
232     : d(other.d)
233 {
234 }
235 
~Address()236 Address::~Address()
237 {
238 }
239 
operator =(const Address & other)240 Address &Address::operator=(const Address &other)
241 {
242     if (this != &other) {
243         d = other.d;
244     }
245 
246     return *this;
247 }
248 
operator ==(const Address & other) const249 bool Address::operator==(const Address &other) const
250 {
251     if (d->mId != other.d->mId) {
252         return false;
253     }
254     if (d->mType != other.d->mType) {
255         return false;
256     }
257     if (d->mPostOfficeBox != other.d->mPostOfficeBox) {
258         return false;
259     }
260     if (d->mExtended != other.d->mExtended) {
261         return false;
262     }
263     if (d->mStreet != other.d->mStreet) {
264         return false;
265     }
266     if (d->mLocality != other.d->mLocality) {
267         return false;
268     }
269     if (d->mRegion != other.d->mRegion) {
270         return false;
271     }
272     if (d->mPostalCode != other.d->mPostalCode) {
273         return false;
274     }
275     if (d->mCountry != other.d->mCountry) {
276         return false;
277     }
278     if (d->mLabel != other.d->mLabel) {
279         return false;
280     }
281 
282     if (d->mGeo != other.d->mGeo) {
283         return false;
284     }
285 
286     return true;
287 }
288 
operator !=(const Address & a) const289 bool Address::operator!=(const Address &a) const
290 {
291     return !(a == *this);
292 }
293 
isEmpty() const294 bool Address::isEmpty() const
295 {
296     return d->mEmpty;
297 }
298 
clear()299 void Address::clear()
300 {
301     *this = Address();
302 }
303 
setId(const QString & id)304 void Address::setId(const QString &id)
305 {
306     d->mEmpty = false;
307     d->mId = id;
308 }
309 
id() const310 QString Address::id() const
311 {
312     return d->mId;
313 }
314 
setType(Type type)315 void Address::setType(Type type)
316 {
317     d->mEmpty = false;
318     d->mType = type;
319 }
320 
type() const321 Address::Type Address::type() const
322 {
323     return d->mType;
324 }
325 
typeLabel(Type type)326 QString Address::typeLabel(Type type)
327 {
328     QString label;
329     const TypeList list = typeList();
330 
331     for (const auto typeFlag : list) {
332         // these are actually flags
333         const TypeFlag flag = static_cast<TypeFlag>(static_cast<int>(typeFlag));
334         if (type & flag) {
335             label.append(QLatin1Char('/') + typeFlagLabel(flag));
336         }
337     }
338 
339     // Remove the first '/'
340     if (!label.isEmpty()) {
341         label.remove(0, 1);
342     }
343 
344     return label;
345 }
346 
typeLabel() const347 QString Address::typeLabel() const
348 {
349     QString label;
350     const TypeList list = typeList();
351 
352     for (const auto f : list) {
353         if ((type() & f) && (f != Pref)) {
354             label.append(QLatin1Char('/') + typeLabel(f));
355         }
356     }
357     // Remove the first '/'
358     if (!label.isEmpty()) {
359         label.remove(0, 1);
360     }
361     return label;
362 }
363 
setPostOfficeBox(const QString & postOfficeBox)364 void Address::setPostOfficeBox(const QString &postOfficeBox)
365 {
366     d->mEmpty = false;
367     d->mPostOfficeBox = postOfficeBox;
368 }
369 
postOfficeBox() const370 QString Address::postOfficeBox() const
371 {
372     return d->mPostOfficeBox;
373 }
374 
postOfficeBoxLabel()375 QString Address::postOfficeBoxLabel()
376 {
377     return i18n("Post Office Box");
378 }
379 
setExtended(const QString & extended)380 void Address::setExtended(const QString &extended)
381 {
382     d->mEmpty = false;
383     d->mExtended = extended;
384 }
385 
extended() const386 QString Address::extended() const
387 {
388     return d->mExtended;
389 }
390 
extendedLabel()391 QString Address::extendedLabel()
392 {
393     return i18n("Extended Address Information");
394 }
395 
setStreet(const QString & street)396 void Address::setStreet(const QString &street)
397 {
398     d->mEmpty = false;
399     d->mStreet = street;
400 }
401 
street() const402 QString Address::street() const
403 {
404     return d->mStreet;
405 }
406 
streetLabel()407 QString Address::streetLabel()
408 {
409     return i18n("Street");
410 }
411 
setLocality(const QString & locality)412 void Address::setLocality(const QString &locality)
413 {
414     d->mEmpty = false;
415     d->mLocality = locality;
416 }
417 
locality() const418 QString Address::locality() const
419 {
420     return d->mLocality;
421 }
422 
localityLabel()423 QString Address::localityLabel()
424 {
425     return i18n("Locality");
426 }
427 
setRegion(const QString & region)428 void Address::setRegion(const QString &region)
429 {
430     d->mEmpty = false;
431     d->mRegion = region;
432 }
433 
region() const434 QString Address::region() const
435 {
436     return d->mRegion;
437 }
438 
regionLabel()439 QString Address::regionLabel()
440 {
441     return i18n("Region");
442 }
443 
setPostalCode(const QString & postalCode)444 void Address::setPostalCode(const QString &postalCode)
445 {
446     d->mEmpty = false;
447     d->mPostalCode = postalCode;
448 }
449 
postalCode() const450 QString Address::postalCode() const
451 {
452     return d->mPostalCode;
453 }
454 
postalCodeLabel()455 QString Address::postalCodeLabel()
456 {
457     return i18n("Postal Code");
458 }
459 
setCountry(const QString & country)460 void Address::setCountry(const QString &country)
461 {
462     d->mEmpty = false;
463     d->mCountry = country;
464 }
465 
country() const466 QString Address::country() const
467 {
468     return d->mCountry;
469 }
470 
countryLabel()471 QString Address::countryLabel()
472 {
473     return i18n("Country");
474 }
475 
setLabel(const QString & label)476 void Address::setLabel(const QString &label)
477 {
478     d->mEmpty = false;
479     d->mLabel = label;
480 }
481 
label() const482 QString Address::label() const
483 {
484     return d->mLabel;
485 }
486 
labelLabel()487 QString Address::labelLabel()
488 {
489     return i18n("Delivery Label");
490 }
491 
typeList()492 Address::TypeList Address::typeList()
493 {
494     static TypeList list;
495 
496     if (list.isEmpty()) {
497         list << Dom << Intl << Postal << Parcel << Home << Work << Pref;
498     }
499 
500     return list;
501 }
502 
typeFlagLabel(TypeFlag type)503 QString Address::typeFlagLabel(TypeFlag type)
504 {
505     switch (type) {
506     case Dom:
507         return i18nc("Address is in home country", "Domestic");
508     case Intl:
509         return i18nc("Address is not in home country", "International");
510     case Postal:
511         return i18nc("Address for delivering letters", "Postal");
512     case Parcel:
513         return i18nc("Address for delivering packages", "Parcel");
514     case Home:
515         return i18nc("Home Address", "Home");
516     case Work:
517         return i18nc("Work Address", "Work");
518     case Pref:
519         return i18n("Preferred Address");
520     }
521     return i18nc("another type of address", "Other");
522 }
523 
setGeo(const Geo & geo)524 void Address::setGeo(const Geo &geo)
525 {
526     d->mEmpty = false;
527     d->mGeo = geo;
528 }
529 
geo() const530 Geo Address::geo() const
531 {
532     return d->mGeo;
533 }
534 
toString() const535 QString Address::toString() const
536 {
537     QString str = QLatin1String("Address {\n");
538     str += QStringLiteral("  IsEmpty: %1\n").arg(d->mEmpty ? QStringLiteral("true") : QStringLiteral("false"));
539     str += QStringLiteral("  Id: %1\n").arg(d->mId);
540     str += QStringLiteral("  Type: %1\n").arg(typeLabel(d->mType));
541     str += QStringLiteral("  Post office box: %1\n").arg(d->mPostOfficeBox);
542     str += QStringLiteral("  Extended: %1\n").arg(d->mExtended);
543     str += QStringLiteral("  Street: %1\n").arg(d->mStreet);
544     str += QStringLiteral("  Locality: %1\n").arg(d->mLocality);
545     str += QStringLiteral("  Region: %1\n").arg(d->mRegion);
546     str += QStringLiteral("  Postal code: %1\n").arg(d->mPostalCode);
547     str += QStringLiteral("  Country: %1\n").arg(d->mCountry);
548     str += QStringLiteral("  Label: %1\n").arg(d->mLabel);
549     str += QStringLiteral("  Geo: %1\n").arg(d->mGeo.toString());
550     str += QLatin1String("}\n");
551 
552     return str;
553 }
554 
addressFormatRc()555 static QString addressFormatRc()
556 {
557     Q_INIT_RESOURCE(kcontacts); // must be called outside of a namespace
558     return QStringLiteral(":/org.kde.kcontacts/addressformatrc");
559 }
560 
formattedAddress(const QString & realName,const QString & orgaName) const561 QString Address::formattedAddress(const QString &realName, const QString &orgaName) const
562 {
563     KCountry countryCode;
564     QString addrTemplate;
565     QString ret;
566 
567     if (country().size() == 2) {
568         countryCode = KCountry::fromAlpha2(country());
569     }
570     if (!countryCode.isValid()) {
571         countryCode = KCountry::fromName(country());
572     }
573     // fall back to our own country
574     if (!countryCode.isValid()) {
575         countryCode = KCountry::fromQLocale(QLocale().country());
576     }
577     static const KConfig entry(addressFormatRc());
578 
579     KConfigGroup group = entry.group(countryCode.alpha2());
580     // decide whether this needs special business address formatting
581     if (orgaName.isEmpty()) {
582         addrTemplate = group.readEntry("AddressFormat");
583     } else {
584         addrTemplate = group.readEntry("BusinessAddressFormat");
585         if (addrTemplate.isEmpty()) {
586             addrTemplate = group.readEntry("AddressFormat");
587         }
588     }
589 
590     // in the case there's no format found at all, default to what we've always
591     // used:
592     if (addrTemplate.isEmpty()) {
593         qCWarning(KCONTACTS_LOG) << "address format database incomplete"
594                                  << "(no format for locale" << countryCode.alpha2() << "found). Using default address formatting.";
595         addrTemplate = QStringLiteral("%0(%n\\n)%0(%cm\\n)%0(%s\\n)%0(PO BOX %p\\n)%0(%l%w%r)%,%z");
596     }
597 
598     // scan
599     parseAddressTemplateSection(addrTemplate, ret, realName, orgaName, *this);
600 
601     // now add the country line if needed (formatting this time according to
602     // the rules of our own system country )
603     if (!country().isEmpty()) {
604         const QString countryName = countryCode.isValid() ? countryCode.name() : country();
605 
606         // Don't include line breaks if country is the only text
607         if (ret.isEmpty()) {
608             return countryName;
609         }
610 
611         KConfigGroup group = entry.group(KCountry::fromQLocale(QLocale().country()).alpha2());
612         QString cpos = group.readEntry("AddressCountryPosition");
613         if (QLatin1String("BELOW") == cpos || cpos.isEmpty()) {
614             ret = ret + QLatin1String("\n\n") + countryName.toUpper();
615         } else if (QLatin1String("below") == cpos) {
616             ret = ret + QLatin1String("\n\n") + countryName;
617         } else if (QLatin1String("ABOVE") == cpos) {
618             ret = countryName.toUpper() + QLatin1String("\n\n") + ret;
619         } else if (QLatin1String("above") == cpos) {
620             ret = countryName + QLatin1String("\n\n") + ret;
621         }
622     }
623 
624     return ret;
625 }
626 
627 #if KCONTACTS_BUILD_DEPRECATED_SINCE(5, 89)
countryToISO(const QString & cname)628 QString Address::countryToISO(const QString &cname)
629 {
630     return KCountry::fromName(cname).alpha2().toLower();
631 }
632 #endif
633 
634 #if KCONTACTS_BUILD_DEPRECATED_SINCE(5, 89)
ISOtoCountry(const QString & ISOname)635 QString Address::ISOtoCountry(const QString &ISOname)
636 {
637     const auto c = KCountry::fromAlpha2(ISOname);
638     return c.isValid() ? c.name() : ISOname;
639 }
640 #endif
641 
642 // clang-format off
operator <<(QDataStream & s,const Address & addr)643 QDataStream &KContacts::operator<<(QDataStream &s, const Address &addr)
644 {
645     return s << addr.d->mId << (uint)addr.d->mType << addr.d->mPostOfficeBox
646              << addr.d->mExtended << addr.d->mStreet << addr.d->mLocality
647              << addr.d->mRegion << addr.d->mPostalCode << addr.d->mCountry
648              << addr.d->mLabel << addr.d->mEmpty << addr.d->mGeo;
649 }
650 
operator >>(QDataStream & s,Address & addr)651 QDataStream &KContacts::operator>>(QDataStream &s, Address &addr)
652 {
653     uint type;
654     s >> addr.d->mId >> type >> addr.d->mPostOfficeBox >> addr.d->mExtended
655     >> addr.d->mStreet >> addr.d->mLocality >> addr.d->mRegion
656     >> addr.d->mPostalCode >> addr.d->mCountry >> addr.d->mLabel
657     >> addr.d->mEmpty >> addr.d->mGeo;
658 
659     addr.d->mType = Address::Type(type);
660 
661     return s;
662 }
663 // clang-format on
664